Skip to content

Commit fb86c3a

Browse files
committed
Add async variant to rpc calls
1 parent 1b1e345 commit fb86c3a

1 file changed

Lines changed: 120 additions & 24 deletions

File tree

frida/core.py

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
from __future__ import annotations
2+
3+
import asyncio
14
import dataclasses
25
import fnmatch
36
import functools
47
import json
58
import sys
69
import threading
710
import traceback
11+
import warnings
812
from types import TracebackType
913
from typing import (
1014
Any,
1115
AnyStr,
16+
Awaitable,
1217
Callable,
1318
Dict,
1419
List,
@@ -156,7 +161,7 @@ def terminate(self) -> None:
156161
self._impl.terminate()
157162

158163

159-
class ScriptExports:
164+
class ScriptExportsSync:
160165
"""
161166
Proxy object that expose all the RPC exports of a script as attributes on this class
162167
@@ -166,7 +171,7 @@ class ScriptExports:
166171
def __init__(self, script: "Script") -> None:
167172
self._script = script
168173

169-
def __getattr__(self, name: str) -> Any:
174+
def __getattr__(self, name: str) -> Callable[..., Any]:
170175
script = self._script
171176
js_name = _to_camel_case(name)
172177

@@ -176,7 +181,33 @@ def method(*args: Any, **kwargs: Any) -> Any:
176181
return method
177182

178183
def __dir__(self) -> List[str]:
179-
return self._script.list_exports()
184+
return self._script.list_exports_sync()
185+
186+
187+
ScriptExports = ScriptExportsSync
188+
189+
190+
class ScriptExportsAsync:
191+
"""
192+
Proxy object that expose all the RPC exports of a script as attributes on this class
193+
194+
A method named exampleMethod in a script will be called with instance.example_method on this object
195+
"""
196+
197+
def __init__(self, script: "Script") -> None:
198+
self._script = script
199+
200+
def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]:
201+
script = self._script
202+
js_name = _to_camel_case(name)
203+
204+
async def method(*args: Any, **kwargs: Any) -> Any:
205+
return await script._rpc_request_async("call", js_name, args, **kwargs)
206+
207+
return method
208+
209+
def __dir__(self) -> List[str]:
210+
return self._script.list_exports_sync()
180211

181212

182213
class ScriptErrorMessage(TypedDict):
@@ -198,22 +229,47 @@ class ScriptPayloadMessage(TypedDict):
198229
ScriptDestroyedCallback = Callable[[], None]
199230

200231

232+
class RPCException(Exception):
233+
"""
234+
Wraps remote errors from the script RPC
235+
"""
236+
237+
def __str__(self) -> str:
238+
return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0])
239+
240+
201241
class Script:
202242
def __init__(self, impl: _frida.Script) -> None:
203-
self.exports = ScriptExports(self)
243+
self.exports_sync = ScriptExportsSync(self)
244+
self.exports_async = ScriptExportsAsync(self)
204245

205246
self._impl = impl
206247

207248
self._on_message_callbacks: List[ScriptMessageCallback] = []
208249
self._log_handler: Callable[[str, str], None] = self.default_log_handler
209250

210-
self._pending: Dict[int, Callable[..., Any]] = {}
251+
self._pending: Dict[
252+
int, Callable[[Optional[Any], Optional[Union[RPCException, _frida.InvalidOperationError]]], None]
253+
] = {}
211254
self._next_request_id = 1
212255
self._cond = threading.Condition()
213256

214257
impl.on("destroyed", self._on_destroyed)
215258
impl.on("message", self._on_message)
216259

260+
@property
261+
def exports(self) -> ScriptExportsSync:
262+
"""
263+
The old way of retrieving the synchronous exports caller
264+
"""
265+
266+
warnings.warn(
267+
"Script.exports will become asynchronous in the future, use the explicit Script.exports_sync instead",
268+
DeprecationWarning,
269+
stacklevel=2,
270+
)
271+
return self.exports_sync
272+
217273
def __repr__(self) -> str:
218274
return repr(self._impl)
219275

@@ -349,7 +405,16 @@ def default_log_handler(self, level: str, text: str) -> None:
349405
else:
350406
print(text, file=sys.stderr)
351407

352-
def list_exports(self) -> List[str]:
408+
async def list_exports_async(self) -> List[str]:
409+
"""
410+
Asynchronously list all the exported attributes from the script's rpc
411+
"""
412+
413+
result = await self._rpc_request_async("list")
414+
assert isinstance(result, list)
415+
return result
416+
417+
def list_exports_sync(self) -> List[str]:
353418
"""
354419
List all the exported attributes from the script's rpc
355420
"""
@@ -358,11 +423,42 @@ def list_exports(self) -> List[str]:
358423
assert isinstance(result, list)
359424
return result
360425

426+
def list_exports(self) -> List[str]:
427+
"""
428+
List all the exported attributes from the script's rpc
429+
"""
430+
431+
warnings.warn(
432+
"Script.list_exports will become asynchronous in the future, use the explicit Script.list_exports_sync instead",
433+
DeprecationWarning,
434+
stacklevel=2,
435+
)
436+
return self.list_exports_sync()
437+
438+
def _rpc_request_async(self, *args: Any) -> asyncio.Future[Any]:
439+
loop = asyncio.get_event_loop()
440+
future: asyncio.Future[Any] = asyncio.Future()
441+
442+
def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None:
443+
if error is not None:
444+
loop.call_soon_threadsafe(future.set_exception, error)
445+
else:
446+
loop.call_soon_threadsafe(future.set_result, value)
447+
448+
request_id = self._append_pending(on_complete)
449+
450+
if not self.is_destroyed:
451+
self._send_rpc_call(request_id, *args)
452+
else:
453+
self._on_destroyed()
454+
455+
return future
456+
361457
@cancellable
362458
def _rpc_request(self, *args: Any) -> Any:
363459
result = RPCResult()
364460

365-
def on_complete(value: Any, error: Union[None, Union[RPCException, _frida.InvalidOperationError]]) -> None:
461+
def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None:
366462
with self._cond:
367463
result.finished = True
368464
result.value = value
@@ -373,15 +469,10 @@ def on_cancelled() -> None:
373469
self._pending.pop(request_id, None)
374470
on_complete(None, None)
375471

376-
with self._cond:
377-
request_id = self._next_request_id
378-
self._next_request_id += 1
379-
self._pending[request_id] = on_complete
472+
request_id = self._append_pending(on_complete)
380473

381474
if not self.is_destroyed:
382-
message = ["frida:rpc", request_id]
383-
message.extend(args)
384-
self.post(message)
475+
self._send_rpc_call(request_id, *args)
385476

386477
cancellable = Cancellable.get_current()
387478
cancel_handler = cancellable.connect(on_cancelled)
@@ -401,7 +492,21 @@ def on_cancelled() -> None:
401492

402493
return result.value
403494

404-
def _on_rpc_message(self, request_id: int, operation: str, params, data) -> None:
495+
def _append_pending(
496+
self, callback: Callable[[Any, Optional[Union[RPCException, _frida.InvalidOperationError]]], None]
497+
) -> int:
498+
with self._cond:
499+
request_id = self._next_request_id
500+
self._next_request_id += 1
501+
self._pending[request_id] = callback
502+
return request_id
503+
504+
def _send_rpc_call(self, request_id: int, *args: Any) -> None:
505+
message = ["frida:rpc", request_id]
506+
message.extend(args)
507+
self.post(message)
508+
509+
def _on_rpc_message(self, request_id: int, operation: str, params: List[Any], data: Optional[Any]) -> None:
405510
if operation in ("ok", "error"):
406511
callback = self._pending.pop(request_id, None)
407512
if callback is None:
@@ -1172,15 +1277,6 @@ def off(self, signal: str, callback: Callable[..., Any]) -> None:
11721277
self._impl.off(signal, callback)
11731278

11741279

1175-
class RPCException(Exception):
1176-
"""
1177-
Wraps remote errors from the script RPC
1178-
"""
1179-
1180-
def __str__(self) -> str:
1181-
return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0])
1182-
1183-
11841280
class EndpointParameters:
11851281
def __init__(
11861282
self,

0 commit comments

Comments
 (0)