|
2 | 2 | from __future__ import absolute_import, division, print_function |
3 | 3 |
|
4 | 4 | import re |
5 | | -import os |
6 | | -import errno |
7 | | -import atexit |
8 | | -import operator |
9 | | -import six |
10 | | -from functools import reduce |
11 | | -import uuid |
12 | | -from six.moves import map |
13 | 5 | import pytest |
14 | 6 | import py |
15 | 7 | from _pytest.monkeypatch import MonkeyPatch |
16 | | -from .compat import Path |
17 | 8 | import attr |
18 | 9 | import shutil |
19 | 10 | import tempfile |
20 | | -import itertools |
21 | | - |
22 | | - |
23 | | -LOCK_TIMEOUT = 60 * 60 * 3 |
24 | | - |
25 | | -get_lock_path = operator.methodcaller("joinpath", ".lock") |
26 | | - |
27 | | - |
28 | | -def find_prefixed(root, prefix): |
29 | | - l_prefix = prefix.lower() |
30 | | - for x in root.iterdir(): |
31 | | - if x.name.lower().startswith(l_prefix): |
32 | | - yield x |
33 | | - |
34 | | - |
35 | | -def extract_suffixes(iter, prefix): |
36 | | - p_len = len(prefix) |
37 | | - for p in iter: |
38 | | - yield p.name[p_len:] |
39 | | - |
40 | | - |
41 | | -def find_suffixes(root, prefix): |
42 | | - return extract_suffixes(find_prefixed(root, prefix), prefix) |
43 | | - |
44 | | - |
45 | | -def parse_num(maybe_num): |
46 | | - try: |
47 | | - return int(maybe_num) |
48 | | - except ValueError: |
49 | | - return -1 |
50 | | - |
51 | | - |
52 | | -def _max(iterable, default): |
53 | | - # needed due to python2.7 lacking the default argument for max |
54 | | - return reduce(max, iterable, default) |
55 | | - |
56 | | - |
57 | | -def make_numbered_dir(root, prefix): |
58 | | - for i in range(10): |
59 | | - # try up to 10 times to create the folder |
60 | | - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) |
61 | | - new_number = max_existing + 1 |
62 | | - new_path = root.joinpath("{}{}".format(prefix, new_number)) |
63 | | - try: |
64 | | - new_path.mkdir() |
65 | | - except Exception: |
66 | | - pass |
67 | | - else: |
68 | | - return new_path |
69 | | - else: |
70 | | - raise EnvironmentError( |
71 | | - "could not create numbered dir with prefix {prefix} in {root})".format( |
72 | | - prefix=prefix, root=root |
73 | | - ) |
74 | | - ) |
75 | | - |
76 | | - |
77 | | -def create_cleanup_lock(p): |
78 | | - lock_path = get_lock_path(p) |
79 | | - try: |
80 | | - fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) |
81 | | - except OSError as e: |
82 | | - if e.errno == errno.EEXIST: |
83 | | - six.raise_from( |
84 | | - EnvironmentError("cannot create lockfile in {path}".format(path=p)), e |
85 | | - ) |
86 | | - else: |
87 | | - raise |
88 | | - else: |
89 | | - pid = os.getpid() |
90 | | - spid = str(pid) |
91 | | - if not isinstance(spid, six.binary_type): |
92 | | - spid = spid.encode("ascii") |
93 | | - os.write(fd, spid) |
94 | | - os.close(fd) |
95 | | - if not lock_path.is_file(): |
96 | | - raise EnvironmentError("lock path got renamed after sucessfull creation") |
97 | | - return lock_path |
98 | | - |
99 | | - |
100 | | -def register_cleanup_lock_removal(lock_path, register=atexit.register): |
101 | | - pid = os.getpid() |
102 | | - |
103 | | - def cleanup_on_exit(lock_path=lock_path, original_pid=pid): |
104 | | - current_pid = os.getpid() |
105 | | - if current_pid != original_pid: |
106 | | - # fork |
107 | | - return |
108 | | - try: |
109 | | - lock_path.unlink() |
110 | | - except (OSError, IOError): |
111 | | - pass |
112 | | - |
113 | | - return register(cleanup_on_exit) |
114 | | - |
115 | | - |
116 | | -def delete_a_numbered_dir(path): |
117 | | - create_cleanup_lock(path) |
118 | | - parent = path.parent |
119 | | - |
120 | | - garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) |
121 | | - path.rename(garbage) |
122 | | - shutil.rmtree(str(garbage), ignore_errors=True) |
123 | | - |
124 | | - |
125 | | -def ensure_deletable(path, consider_lock_dead_if_created_before): |
126 | | - lock = get_lock_path(path) |
127 | | - if not lock.exists(): |
128 | | - return True |
129 | | - try: |
130 | | - lock_time = lock.stat().st_mtime |
131 | | - except Exception: |
132 | | - return False |
133 | | - else: |
134 | | - if lock_time < consider_lock_dead_if_created_before: |
135 | | - lock.unlink() |
136 | | - return True |
137 | | - else: |
138 | | - return False |
139 | | - |
140 | | - |
141 | | -def try_cleanup(path, consider_lock_dead_if_created_before): |
142 | | - if ensure_deletable(path, consider_lock_dead_if_created_before): |
143 | | - delete_a_numbered_dir(path) |
144 | | - |
145 | | - |
146 | | -def cleanup_candidates(root, prefix, keep): |
147 | | - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), -1) |
148 | | - max_delete = max_existing - keep |
149 | | - paths = find_prefixed(root, prefix) |
150 | | - paths, paths2 = itertools.tee(paths) |
151 | | - numbers = map(parse_num, extract_suffixes(paths2, prefix)) |
152 | | - for path, number in zip(paths, numbers): |
153 | | - if number <= max_delete: |
154 | | - yield path |
155 | | - |
156 | | - |
157 | | -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): |
158 | | - for path in cleanup_candidates(root, prefix, keep): |
159 | | - try_cleanup(path, consider_lock_dead_if_created_before) |
160 | | - for path in root.glob("garbage-*"): |
161 | | - try_cleanup(path, consider_lock_dead_if_created_before) |
162 | | - |
163 | | - |
164 | | -def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): |
165 | | - e = None |
166 | | - for i in range(10): |
167 | | - try: |
168 | | - p = make_numbered_dir(root, prefix) |
169 | | - lock_path = create_cleanup_lock(p) |
170 | | - register_cleanup_lock_removal(lock_path) |
171 | | - except Exception as e: |
172 | | - pass |
173 | | - else: |
174 | | - consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout |
175 | | - cleanup_numbered_dir( |
176 | | - root=root, |
177 | | - prefix=prefix, |
178 | | - keep=keep, |
179 | | - consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, |
180 | | - ) |
181 | | - return p |
182 | | - assert e is not None |
183 | | - raise e |
| 11 | +from .pathlib import Path, make_numbered_dir, make_numbered_dir_with_cleanup |
184 | 12 |
|
185 | 13 |
|
186 | 14 | @attr.s |
|
0 commit comments