1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
4
5
6import atexit
7import errno
8import os
9import tempfile
10import time
11
12from calibre.constants import cache_dir, iswindows
13from calibre.ptempfile import remove_dir
14from calibre.utils.monotonic import monotonic
15
16TDIR_LOCK = 'tdir-lock'
17
18if iswindows:
19    from calibre.utils.lock import windows_open
20
21    def lock_tdir(path):
22        return windows_open(os.path.join(path, TDIR_LOCK))
23
24    def unlock_file(fobj):
25        fobj.close()
26
27    def remove_tdir(path, lock_file):
28        lock_file.close()
29        remove_dir(path)
30
31    def is_tdir_locked(path):
32        try:
33            with windows_open(os.path.join(path, TDIR_LOCK)):
34                pass
35        except OSError:
36            return True
37        return False
38else:
39    import fcntl
40    from calibre.utils.ipc import eintr_retry_call
41
42    def lock_tdir(path):
43        lf = os.path.join(path, TDIR_LOCK)
44        f = lopen(lf, 'w')
45        eintr_retry_call(fcntl.lockf, f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
46        return f
47
48    def unlock_file(fobj):
49        from calibre.utils.ipc import eintr_retry_call
50        eintr_retry_call(fcntl.lockf, fobj.fileno(), fcntl.LOCK_UN)
51        fobj.close()
52
53    def remove_tdir(path, lock_file):
54        lock_file.close()
55        remove_dir(path)
56
57    def is_tdir_locked(path):
58        lf = os.path.join(path, TDIR_LOCK)
59        f = lopen(lf, 'w')
60        try:
61            eintr_retry_call(fcntl.lockf, f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
62            eintr_retry_call(fcntl.lockf, f.fileno(), fcntl.LOCK_UN)
63            return False
64        except OSError:
65            return True
66        finally:
67            f.close()
68
69
70def tdirs_in(b):
71    try:
72        tdirs = os.listdir(b)
73    except OSError as e:
74        if e.errno != errno.ENOENT:
75            raise
76        tdirs = ()
77    for x in tdirs:
78        x = os.path.join(b, x)
79        if os.path.isdir(x):
80            yield x
81
82
83def clean_tdirs_in(b):
84    # Remove any stale tdirs left by previous program crashes
85    for q in tdirs_in(b):
86        if not is_tdir_locked(q):
87            remove_dir(q)
88
89
90def retry_lock_tdir(path, timeout=30, sleep=0.1):
91    st = monotonic()
92    while True:
93        try:
94            return lock_tdir(path)
95        except Exception:
96            if monotonic() - st > timeout:
97                raise
98            time.sleep(sleep)
99
100
101def tdir_in_cache(base):
102    ''' Create a temp dir inside cache_dir/base. The created dir is robust
103    against application crashes. i.e. it will be cleaned up the next time the
104    application starts, even if it was left behind by a previous crash. '''
105    b = os.path.join(os.path.realpath(cache_dir()), base)
106    try:
107        os.makedirs(b)
108    except OSError as e:
109        if e.errno != errno.EEXIST:
110            raise
111    global_lock = retry_lock_tdir(b)
112    try:
113        if b not in tdir_in_cache.scanned:
114            tdir_in_cache.scanned.add(b)
115            try:
116                clean_tdirs_in(b)
117            except Exception:
118                import traceback
119                traceback.print_exc()
120        tdir = tempfile.mkdtemp(dir=b)
121        lock_data = lock_tdir(tdir)
122        atexit.register(remove_tdir, tdir, lock_data)
123        tdir = os.path.join(tdir, 'a')
124        os.mkdir(tdir)
125        return tdir
126    finally:
127        unlock_file(global_lock)
128
129
130tdir_in_cache.scanned = set()
131