5858import os
5959import shutil
6060import tempfile
61+ import time
6162from pathlib import Path
6263from typing import IO , TYPE_CHECKING , cast
6364from urllib import parse
7980 # advisory file locking for posix
8081 import fcntl
8182
82- def _lock_file (f : IO ) -> None :
83- if f .writable ():
83+ @contextlib .contextmanager
84+ def _lock_file (path : str ) -> Iterator [IO ]:
85+ with open (path , "wb" ) as f :
8486 fcntl .lockf (f , fcntl .LOCK_EX )
87+ yield f
8588
8689except ModuleNotFoundError :
87- # Windows file locking
90+ # Windows file locking, in belt-and-suspenders-from-Temu style:
91+ # Use a loop that tries to open the lockfile for 30 secs, but also
92+ # use msvcrt.locking().
93+ # * since open() usually just fails when another process has the file open
94+ # msvcrt.locking() almost never gets called when there is a lock. open()
95+ # sometimes succeeds for multiple processes though
96+ # * msvcrt.locking() does not even block until file is available: it just
97+ # tries once per second in a non-blocking manner for 10 seconds. So if
98+ # another process keeps opening the file it's unlikely that we actually
99+ # get the lock
88100 import msvcrt
89101
90- def _lock_file (f : IO ) -> None :
91- # On Windows we lock a byte range and file must not be empty
92- f .write (b"\0 " )
93- f .flush ()
94- f .seek (0 )
95-
96- msvcrt .locking (f .fileno (), msvcrt .LK_LOCK , 1 )
102+ @contextlib .contextmanager
103+ def _lock_file (path : str ) -> Iterator [IO ]:
104+ err = None
105+ locked = False
106+ for _ in range (100 ):
107+ try :
108+ with open (path , "wb" ) as f :
109+ msvcrt .locking (f .fileno (), msvcrt .LK_LOCK , 1 )
110+ locked = True
111+ yield f
112+ return
113+ except FileNotFoundError :
114+ # could be from yield or from open() -- either way we bail
115+ raise
116+ except OSError as e :
117+ if locked :
118+ # yield has raised, let's not continue loop
119+ raise e
120+ err = e
121+ logger .warning ("Unsuccessful lock attempt for %s: %s" , path , e )
122+ time .sleep (0.3 )
123+
124+ # raise the last failure if we never got a lock
125+ if err is not None :
126+ raise err
97127
98128
99129class Updater :
@@ -153,6 +183,10 @@ def __init__(
153183 f"got '{ self .config .envelope_type } '"
154184 )
155185
186+ # Ensure the whole metadata directory structure exists
187+ rootdir = Path (self ._dir , "root_history" )
188+ rootdir .mkdir (exist_ok = True , parents = True )
189+
156190 with self ._lock_metadata ():
157191 if not bootstrap :
158192 # if no root was provided, use the cached non-versioned root
@@ -168,15 +202,11 @@ def __init__(
168202 @contextlib .contextmanager
169203 def _lock_metadata (self ) -> Iterator [None ]:
170204 """Context manager for locking the metadata directory."""
171- # Ensure the whole metadata directory structure exists
172- rootdir = Path (self ._dir , "root_history" )
173- rootdir .mkdir (exist_ok = True , parents = True )
174205
175- with open (os .path .join (self ._dir , ".lock" ), "wb" ) as f :
176- logger .debug ("Getting metadata lock..." )
177- _lock_file (f )
206+ logger .debug ("Getting metadata lock..." )
207+ with _lock_file (os .path .join (self ._dir , ".lock" )):
178208 yield
179- logger .debug ("Releasing metadata lock" )
209+ logger .debug ("Released metadata lock" )
180210
181211 def refresh (self ) -> None :
182212 """Refresh top-level metadata.
@@ -337,8 +367,7 @@ def download_target(
337367 targetinfo .verify_length_and_hashes (target_file )
338368
339369 target_file .seek (0 )
340- with open (filepath , "wb" ) as destination_file :
341- _lock_file (destination_file )
370+ with _lock_file (filepath ) as destination_file :
342371 shutil .copyfileobj (target_file , destination_file )
343372
344373 logger .debug ("Downloaded target %s" , targetinfo .path )
0 commit comments