1+ from __future__ import annotations
2+
3+ import asyncio
14import dataclasses
25import fnmatch
36import functools
47import json
58import sys
69import threading
710import traceback
11+ import warnings
812from types import TracebackType
913from 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
182213class ScriptErrorMessage (TypedDict ):
@@ -198,22 +229,47 @@ class ScriptPayloadMessage(TypedDict):
198229ScriptDestroyedCallback = 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+
201241class 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-
11841280class EndpointParameters :
11851281 def __init__ (
11861282 self ,
0 commit comments