Skip to content

Commit fa61b91

Browse files
Merge pull request #752 from screamerbg/f/thread_safety
Fix: Caching thread safety
2 parents 63e3fae + d0c1dd6 commit fa61b91

File tree

1 file changed

+118
-3
lines changed

1 file changed

+118
-3
lines changed

mbed/mbed.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@
4343
import errno
4444
import ctypes
4545
from itertools import chain, repeat
46+
import time
4647
import zipfile
4748
import argparse
49+
from random import randint
50+
from contextlib import contextmanager
4851

4952

5053
# Application version
@@ -155,7 +158,10 @@ def log(msg, is_error=False):
155158
sys.stderr.write(msg) if is_error else sys.stdout.write(msg)
156159

157160
def message(msg):
158-
return "[mbed] %s\n" % msg
161+
if very_verbose:
162+
return "[mbed-%s] %s\n" % (os.getpid(), msg)
163+
else:
164+
return "[mbed] %s\n" % msg
159165

160166
def info(msg, level=1):
161167
if level <= 0 or verbose:
@@ -1223,7 +1229,8 @@ def clone(self, url, path, rev=None, depth=None, protocol=None, offline=False, *
12231229
os.makedirs(os.path.split(path)[0])
12241230

12251231
info("Carbon copy from \"%s\" to \"%s\"" % (cache, path))
1226-
shutil.copytree(cache, path)
1232+
with self.cache_lock_held(url):
1233+
shutil.copytree(cache, path)
12271234

12281235
with cd(path):
12291236
scm.seturl(formaturl(url, protocol))
@@ -1253,7 +1260,8 @@ def clone(self, url, path, rev=None, depth=None, protocol=None, offline=False, *
12531260
self.url = url
12541261
self.path = os.path.abspath(path)
12551262
self.ignores()
1256-
self.set_cache(url)
1263+
with self.cache_lock_held(url):
1264+
self.set_cache(url)
12571265
return True
12581266

12591267
if offline:
@@ -1336,6 +1344,113 @@ def set_cache(self, url):
13361344
warning("Unable to cache \"%s\" to \"%s\"" % (self.path, cpath))
13371345
return False
13381346

1347+
def cache_lock(self, url):
1348+
cpath = self.url2cachedir(url)
1349+
if not cpath:
1350+
return False
1351+
1352+
if not os.path.isdir(cpath):
1353+
os.makedirs(cpath)
1354+
1355+
lock_dir = os.path.join(cpath, '.lock')
1356+
lock_file = os.path.join(lock_dir, 'pid')
1357+
timeout = 300
1358+
1359+
for i in range(timeout):
1360+
if i:
1361+
time.sleep(1)
1362+
1363+
if os.path.exists(lock_dir):
1364+
try:
1365+
if os.path.isfile(lock_file):
1366+
with open(lock_file, 'r') as f:
1367+
pid = f.read(8)
1368+
if not pid:
1369+
if int(os.path.getmtime(lock_file)) + timeout < int(time.time()):
1370+
info("Cache lock file exists, but is empty. Cleaning up")
1371+
os.remove(lock_file)
1372+
os.rmdir(lock_dir)
1373+
elif int(pid) != os.getpid() and self.pid_exists(pid):
1374+
info("Cache lock file exists and process %s is alive." % pid)
1375+
else:
1376+
info("Cache lock file exists, but %s is dead. Cleaning up" % pid)
1377+
os.remove(lock_file)
1378+
os.rmdir(lock_dir)
1379+
else:
1380+
os.rmdir(lock_dir)
1381+
continue
1382+
except (OSError) as e:
1383+
continue
1384+
else:
1385+
try:
1386+
os.mkdir(lock_dir)
1387+
with open(lock_file, 'w') as f:
1388+
info("Writing cache lock file %s for pid %s" % (lock_file, os.getpid()))
1389+
f.write(str(os.getpid()))
1390+
f.flush()
1391+
os.fsync(f)
1392+
break
1393+
except (OSError) as e:
1394+
## Windows:
1395+
## <type 'exceptions.WindowsError'> 17 [Error 183] Cannot create a file when that file already exists: 'testing'
1396+
## or when concurrent: 13 WindowsError(5, 'Access is denied')
1397+
## Linux: <type 'exceptions.OSError'> 17 [Errno 17] File exists: 'testing'
1398+
## or when concurrent & virtualbox 71, OSError(71, 'Protocol error')
1399+
## or when full: 28, OSError(28, 'No space left on device')
1400+
if e.errno in (17,13,71,28):
1401+
continue
1402+
else:
1403+
raise e
1404+
else:
1405+
error("Exceeded 5 minutes limit while waiting for other process to finish caching")
1406+
return True
1407+
1408+
def cache_unlock(self, url):
1409+
cpath = self.url2cachedir(url)
1410+
if not cpath:
1411+
return False
1412+
1413+
lock_dir = os.path.join(cpath, '.lock')
1414+
lock_file = os.path.join(lock_dir, 'pid')
1415+
try:
1416+
if os.path.exists(lock_dir):
1417+
if os.path.isfile(lock_file):
1418+
try:
1419+
with open(lock_file, 'r') as f:
1420+
pid = f.read(8)
1421+
if int(pid) != os.getpid():
1422+
error("Cache lock file exists with a different pid (\"%s\" vs \"%s\")" % (pid, os.getpid()))
1423+
else:
1424+
info("Cache lock file exists with my pid (\"%s\"). Cleaning up." % (pid))
1425+
except OSError:
1426+
error("Unable to unlock cache dir \"%s\"" % (cpath))
1427+
os.remove(lock_file)
1428+
os.rmdir(lock_dir)
1429+
except (OSError) as e:
1430+
pass
1431+
return True
1432+
1433+
@contextmanager
1434+
def cache_lock_held(self, url):
1435+
self.cache_lock(url)
1436+
try:
1437+
yield
1438+
finally:
1439+
self.cache_unlock(url)
1440+
1441+
def pid_exists(self, pid):
1442+
try:
1443+
os.kill(int(pid), 0)
1444+
except OSError as err:
1445+
if err.errno == errno.ESRCH:
1446+
return False
1447+
elif err.errno == errno.EPERM:
1448+
return True
1449+
else:
1450+
raise err
1451+
else:
1452+
return True
1453+
13391454
def can_update(self, clean, clean_deps):
13401455
err = None
13411456
if (self.is_local or self.url is None) and not clean_deps:

0 commit comments

Comments
 (0)