22
33from __future__ import annotations
44
5+ import asyncio
56import base64
67import locale
78import logging
@@ -31,7 +32,7 @@ class LocalAnisetteMapping(TypedDict):
3132 """JSON mapping representing state of a local Anisette provider."""
3233
3334 type : Literal ["aniLocal" ]
34- prov_data : str
35+ prov_data : str | None
3536
3637
3738AnisetteMapping = Union [RemoteAnisetteMapping , LocalAnisetteMapping ]
@@ -286,7 +287,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, util.abc.Serializable[LocalAni
286287 def __init__ (
287288 self ,
288289 * ,
289- state_blob : BinaryIO | None = None ,
290+ state_blob : BytesIO | None = None ,
290291 libs_path : str | Path | None = None ,
291292 ) -> None :
292293 """Initialize the provider."""
@@ -295,41 +296,75 @@ def __init__(
295296 if isinstance (libs_path , str ):
296297 libs_path = Path (libs_path )
297298
298- if libs_path is None or not libs_path .is_file ():
299+ # we do not yet initialize Anisette in order to prevent blocking the event loop,
300+ # since the anisette library will download the required libraries synchronously.
301+ self ._ani : Anisette | None = None
302+
303+ self ._ani_data : AnisetteHeaders | None = None
304+ self ._libs_path : Path | None = libs_path
305+ self ._state_blob : BytesIO | None = state_blob
306+
307+ @property
308+ def _is_new_session (self ) -> bool :
309+ return self ._state_blob is None
310+
311+ async def _get_ani (self ) -> Anisette :
312+ if self ._ani is not None :
313+ return self ._ani
314+
315+ if self ._libs_path is None or not self ._libs_path .is_file ():
299316 logger .info (
300317 "The Anisette engine will download libraries required for operation, "
301318 "this may take a few seconds..." ,
302319 )
303- if libs_path is None :
320+ if self . _libs_path is None :
304321 logger .info (
305322 "To speed up future local Anisette initializations, "
306323 "provide a filesystem path to load the libraries from." ,
307324 )
308325
309326 files : list [BinaryIO | Path ] = []
310- if state_blob is not None :
311- files .append (state_blob )
312- if libs_path is not None and libs_path .exists ():
313- files .append (libs_path )
327+ if self . _state_blob is not None :
328+ files .append (self . _state_blob )
329+ if self . _libs_path is not None and self . _libs_path .exists ():
330+ files .append (self . _libs_path )
314331
315- self ._ani = Anisette .load (* files )
316- self ._ani_data : AnisetteHeaders | None = None
317- self ._libs_path : Path | None = libs_path
332+ loop = asyncio .get_running_loop ()
333+ ani = await loop .run_in_executor (None , Anisette .load , * files )
334+ is_provisioned = await loop .run_in_executor (None , lambda : ani .is_provisioned )
335+
336+ if self ._libs_path is not None :
337+ ani .save_libs (self ._libs_path )
318338
319- if libs_path is not None :
320- self ._ani .save_libs (libs_path )
321- if state_blob is not None and not self ._ani .is_provisioned :
339+ if not self ._is_new_session and not is_provisioned :
322340 logger .warning (
323341 "The Anisette state that was loaded has not yet been provisioned. "
324342 "Was the previous session saved properly?" ,
325343 )
326344
345+ # pre-provision to ensure that the VM has initialized
346+ await loop .run_in_executor (None , ani .provision )
347+
348+ self ._ani = ani
349+ return ani
350+
327351 @override
328352 def to_json (self , dst : str | Path | None = None , / ) -> LocalAnisetteMapping :
329353 """See :meth:`BaseAnisetteProvider.serialize`."""
330- with BytesIO () as buf :
331- self ._ani .save_provisioning (buf )
332- prov_data = base64 .b64encode (buf .getvalue ()).decode ("utf-8" )
354+ if self ._ani is None :
355+ # Anisette has not been called yet, so the future has not yet resolved.
356+ # We don't want to wait here, so we just return the original state blob.
357+ # If the state blob is None, this means we have a new session that has not
358+ # been provisioned yet, so we will not save the provisioning data.
359+ if self ._state_blob is None :
360+ prov_data = None
361+ else :
362+ prov_data = base64 .b64encode (self ._state_blob .getvalue ()).decode ("utf-8" )
363+ else :
364+ # Anisette has been initialized, so we can save the provisioning data.
365+ with BytesIO () as buf :
366+ self ._ani .save_provisioning (buf )
367+ prov_data = base64 .b64encode (buf .getvalue ()).decode ("utf-8" )
333368
334369 return util .files .save_and_return_json (
335370 {
@@ -352,7 +387,8 @@ def from_json(
352387
353388 assert val ["type" ] == "aniLocal"
354389
355- state_blob = BytesIO (base64 .b64decode (val ["prov_data" ]))
390+ prov_data = val ["prov_data" ]
391+ state_blob = None if prov_data is None else BytesIO (base64 .b64decode (prov_data ))
356392
357393 return cls (state_blob = state_blob , libs_path = libs_path )
358394
@@ -365,7 +401,12 @@ async def get_headers(
365401 with_client_info : bool = False ,
366402 ) -> dict [str , str ]:
367403 """See :meth:`BaseAnisetteProvider.get_headers`."""
368- self ._ani_data = self ._ani .get_data ()
404+ ani = await self ._get_ani ()
405+
406+ # run in executor to prevent blocking the event loop,
407+ # since get_data may make blocking network requests.
408+ loop = asyncio .get_running_loop ()
409+ self ._ani_data = await loop .run_in_executor (None , ani .get_data )
369410
370411 return await super ().get_headers (user_id , device_id , serial , with_client_info )
371412
0 commit comments