omlish 0.0.0.dev226__py3-none-any.whl → 0.0.0.dev227__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omlish/__about__.py +2 -2
- omlish/diag/lslocks.py +4 -4
- omlish/diag/lsof.py +2 -2
- omlish/os/pidfiles/manager.py +4 -1
- omlish/os/pidfiles/pidfile.py +31 -11
- omlish/os/pidfiles/pinning.py +246 -0
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/RECORD +12 -11
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev227.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/diag/lslocks.py
CHANGED
@@ -19,7 +19,7 @@ from ..subprocesses.run import SubprocessRunOutput
|
|
19
19
|
|
20
20
|
|
21
21
|
@dc.dataclass(frozen=True)
|
22
|
-
class
|
22
|
+
class LslocksItem:
|
23
23
|
"""https://manpages.ubuntu.com/manpages/lunar/man8/lslocks.8.html"""
|
24
24
|
|
25
25
|
command: str
|
@@ -38,7 +38,7 @@ class LsLocksItem:
|
|
38
38
|
|
39
39
|
|
40
40
|
@dc.dataclass(frozen=True)
|
41
|
-
class
|
41
|
+
class LslocksCommand(SubprocessRunnable[ta.List[LslocksItem]]):
|
42
42
|
pid: ta.Optional[int] = None
|
43
43
|
no_inaccessible: bool = False
|
44
44
|
|
@@ -56,9 +56,9 @@ class LsLocksCommand(SubprocessRunnable):
|
|
56
56
|
stderr='devnull',
|
57
57
|
)
|
58
58
|
|
59
|
-
def handle_run_output(self, output: SubprocessRunOutput) -> ta.List[
|
59
|
+
def handle_run_output(self, output: SubprocessRunOutput) -> ta.List[LslocksItem]:
|
60
60
|
buf = check.not_none(output.stdout).decode().strip()
|
61
61
|
if not buf:
|
62
62
|
return []
|
63
63
|
obj = json.loads(buf)
|
64
|
-
return unmarshal_obj(obj['locks'], ta.List[
|
64
|
+
return unmarshal_obj(obj['locks'], ta.List[LslocksItem])
|
omlish/diag/lsof.py
CHANGED
@@ -151,7 +151,6 @@ class LsofItem:
|
|
151
151
|
|
152
152
|
@classmethod
|
153
153
|
def from_prefixes(cls, dct: ta.Mapping[str, ta.Any]) -> 'LsofItem':
|
154
|
-
print(dct)
|
155
154
|
kw: ta.Dict[str, ta.Any] = {
|
156
155
|
fld.name: val
|
157
156
|
for pfx, val in dct.items()
|
@@ -216,7 +215,8 @@ LsofItem._DEFAULT_PREFIXES = ''.join(LsofItem._FIELDS_BY_PREFIX) # noqa
|
|
216
215
|
|
217
216
|
|
218
217
|
@dc.dataclass(frozen=True)
|
219
|
-
class
|
218
|
+
class \
|
219
|
+
LsofCommand(SubprocessRunnable[ta.List[LsofItem]]):
|
220
220
|
pid: ta.Optional[int] = None
|
221
221
|
file: ta.Optional[str] = None
|
222
222
|
|
omlish/os/pidfiles/manager.py
CHANGED
@@ -15,7 +15,10 @@ from .pidfile import Pidfile
|
|
15
15
|
|
16
16
|
class _PidfileManager:
|
17
17
|
"""
|
18
|
-
Manager for controlled inheritance of Pidfiles across forks.
|
18
|
+
Manager for controlled inheritance of Pidfiles across forks in the presence of multiple threads. There is of course
|
19
|
+
no safe or correct way to mix the use of fork and multiple active threads, and one should never write code which
|
20
|
+
does so, but in the Real World one may still find oneself in such a situation outside of their control (such as when
|
21
|
+
running under Pycharm's debugger which forces the use of forked multiprocessing).
|
19
22
|
|
20
23
|
Not implemented as an instantiated class as there is no way to unregister at_fork listeners, and because Pidfiles
|
21
24
|
may be pickled and there must be no possibility of accidentally unpickling and instantiating a new instance of the
|
omlish/os/pidfiles/pidfile.py
CHANGED
@@ -109,7 +109,7 @@ class Pidfile:
|
|
109
109
|
del self._f
|
110
110
|
return True
|
111
111
|
|
112
|
-
def
|
112
|
+
def try_acquire_lock(self) -> bool:
|
113
113
|
try:
|
114
114
|
fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
115
115
|
return True
|
@@ -117,14 +117,29 @@ class Pidfile:
|
|
117
117
|
except OSError:
|
118
118
|
return False
|
119
119
|
|
120
|
-
|
121
|
-
|
122
|
-
|
120
|
+
#
|
121
|
+
|
122
|
+
class Error(Exception):
|
123
|
+
pass
|
124
|
+
|
125
|
+
class LockedError(Error):
|
126
|
+
pass
|
127
|
+
|
128
|
+
def acquire_lock(self) -> None:
|
129
|
+
if not self.try_acquire_lock():
|
130
|
+
raise self.LockedError
|
131
|
+
|
132
|
+
class NotLockedError(Error):
|
133
|
+
pass
|
134
|
+
|
135
|
+
def ensure_cannot_lock(self) -> None:
|
136
|
+
if self.try_acquire_lock():
|
137
|
+
raise self.NotLockedError
|
123
138
|
|
124
139
|
#
|
125
140
|
|
126
141
|
def write(self, pid: ta.Optional[int] = None) -> None:
|
127
|
-
self.
|
142
|
+
self.acquire_lock()
|
128
143
|
|
129
144
|
if pid is None:
|
130
145
|
pid = os.getpid()
|
@@ -135,18 +150,23 @@ class Pidfile:
|
|
135
150
|
self._f.flush()
|
136
151
|
|
137
152
|
def clear(self) -> None:
|
138
|
-
self.
|
153
|
+
self.acquire_lock()
|
139
154
|
|
140
155
|
self._f.seek(0)
|
141
156
|
self._f.truncate()
|
142
157
|
|
143
|
-
|
144
|
-
|
145
|
-
|
158
|
+
#
|
159
|
+
|
160
|
+
def read(self) -> ta.Optional[int]:
|
161
|
+
self.ensure_cannot_lock()
|
146
162
|
|
147
163
|
self._f.seek(0)
|
148
|
-
|
164
|
+
buf = self._f.read()
|
165
|
+
if not buf:
|
166
|
+
return None
|
167
|
+
return int(buf)
|
149
168
|
|
150
169
|
def kill(self, sig: int = signal.SIGTERM) -> None:
|
151
|
-
pid
|
170
|
+
if (pid := self.read()) is None:
|
171
|
+
raise self.Error(f'Pidfile locked but empty')
|
152
172
|
os.kill(pid, sig)
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
Strategies:
|
5
|
+
- linux
|
6
|
+
- get pid of owner (lslocks or F_GETLK)
|
7
|
+
- open pidfd to owner pid
|
8
|
+
- re-check pid of owner
|
9
|
+
- darwin
|
10
|
+
- get pids of referrers (lsof)
|
11
|
+
- read pid from file
|
12
|
+
- ensure pid is in referrers
|
13
|
+
- optionally loop
|
14
|
+
- ? setup pid death watcher? still a race
|
15
|
+
"""
|
16
|
+
import abc
|
17
|
+
import contextlib
|
18
|
+
import os.path
|
19
|
+
import shutil
|
20
|
+
import sys
|
21
|
+
import time
|
22
|
+
import typing as ta
|
23
|
+
|
24
|
+
from ...diag.lslocks import LslocksCommand
|
25
|
+
from ...diag.lsof import LsofCommand
|
26
|
+
from ...lite.timeouts import Timeout
|
27
|
+
from ...lite.timeouts import TimeoutLike
|
28
|
+
from ...subprocesses.sync import subprocesses # noqa
|
29
|
+
from .pidfile import Pidfile
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
|
34
|
+
|
35
|
+
class PidfilePinner(abc.ABC):
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
*,
|
39
|
+
sleep_s: float = .1,
|
40
|
+
) -> None:
|
41
|
+
super().__init__()
|
42
|
+
|
43
|
+
self._sleep_s = sleep_s
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
@abc.abstractmethod
|
47
|
+
def is_available(cls) -> bool:
|
48
|
+
raise NotImplementedError
|
49
|
+
|
50
|
+
class NoOwnerError(Exception):
|
51
|
+
pass
|
52
|
+
|
53
|
+
@abc.abstractmethod
|
54
|
+
def _pin_pidfile_owner(self, pidfile: Pidfile, timeout: Timeout) -> ta.ContextManager[int]:
|
55
|
+
raise NotImplementedError
|
56
|
+
|
57
|
+
@contextlib.contextmanager
|
58
|
+
def pin_pidfile_owner(
|
59
|
+
self,
|
60
|
+
path: str,
|
61
|
+
*,
|
62
|
+
timeout: ta.Optional[TimeoutLike] = None,
|
63
|
+
) -> ta.Iterator[int]:
|
64
|
+
timeout = Timeout.of(timeout)
|
65
|
+
|
66
|
+
if not os.path.isfile(path):
|
67
|
+
raise self.NoOwnerError
|
68
|
+
|
69
|
+
with Pidfile(path, inheritable=False) as pf:
|
70
|
+
try:
|
71
|
+
with self._pin_pidfile_owner(pf, timeout) as pid:
|
72
|
+
yield pid
|
73
|
+
|
74
|
+
except Pidfile.NotLockedError:
|
75
|
+
raise self.NoOwnerError from None
|
76
|
+
|
77
|
+
@classmethod
|
78
|
+
def default_impl(cls) -> ta.Type['PidfilePinner']:
|
79
|
+
for impl in [
|
80
|
+
LslocksPidfdPidfilePinner,
|
81
|
+
LsofPidfilePinner,
|
82
|
+
]:
|
83
|
+
if impl.is_available():
|
84
|
+
return impl
|
85
|
+
return UnverifiedPidfilePinner
|
86
|
+
|
87
|
+
|
88
|
+
##
|
89
|
+
|
90
|
+
|
91
|
+
class UnverifiedPidfilePinner(PidfilePinner):
|
92
|
+
@classmethod
|
93
|
+
def is_available(cls) -> bool:
|
94
|
+
return True
|
95
|
+
|
96
|
+
@contextlib.contextmanager
|
97
|
+
def _pin_pidfile_owner(self, pidfile: Pidfile, timeout: Timeout) -> ta.Iterator[int]:
|
98
|
+
while (pid := pidfile.read()) is None:
|
99
|
+
time.sleep(self._sleep_s)
|
100
|
+
timeout()
|
101
|
+
|
102
|
+
yield pid
|
103
|
+
|
104
|
+
|
105
|
+
##
|
106
|
+
|
107
|
+
|
108
|
+
class LsofPidfilePinner(PidfilePinner):
|
109
|
+
"""
|
110
|
+
Fundamentally wrong, but still better than nothing. Simply reads the file contents and ensures a valid contained pid
|
111
|
+
has the file open via `lsof`.
|
112
|
+
"""
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def is_available(cls) -> bool:
|
116
|
+
return shutil.which('lsof') is not None
|
117
|
+
|
118
|
+
def _try_read_and_verify(self, pf: Pidfile, timeout: Timeout) -> ta.Optional[int]:
|
119
|
+
if (initial_pid := pf.read()) is None:
|
120
|
+
return None
|
121
|
+
|
122
|
+
lsof_output = LsofCommand(
|
123
|
+
# pid=initial_pid,
|
124
|
+
file=os.path.abspath(pf.path),
|
125
|
+
).run(
|
126
|
+
timeout=timeout,
|
127
|
+
)
|
128
|
+
|
129
|
+
lsof_pids: ta.Set[int] = set()
|
130
|
+
for li in lsof_output:
|
131
|
+
if li.pid is None:
|
132
|
+
continue
|
133
|
+
try:
|
134
|
+
li_pid = int(li.pid)
|
135
|
+
except ValueError:
|
136
|
+
continue
|
137
|
+
lsof_pids.add(li_pid)
|
138
|
+
|
139
|
+
if initial_pid not in lsof_pids:
|
140
|
+
return None
|
141
|
+
|
142
|
+
if (reread_pid := pf.read()) is None or reread_pid != initial_pid:
|
143
|
+
return None
|
144
|
+
|
145
|
+
return reread_pid
|
146
|
+
|
147
|
+
@contextlib.contextmanager
|
148
|
+
def _pin_pidfile_owner(self, pidfile: Pidfile, timeout: Timeout) -> ta.Iterator[int]:
|
149
|
+
while (pid := self._try_read_and_verify(pidfile, timeout)) is None:
|
150
|
+
time.sleep(self._sleep_s)
|
151
|
+
timeout()
|
152
|
+
|
153
|
+
yield pid
|
154
|
+
|
155
|
+
|
156
|
+
##
|
157
|
+
|
158
|
+
|
159
|
+
class LslocksPidfdPidfilePinner(PidfilePinner):
|
160
|
+
"""
|
161
|
+
Finds the locking pid via `lslocks`, opens a pidfd, then re-runs `lslocks` and rechecks the locking pid is the same.
|
162
|
+
"""
|
163
|
+
|
164
|
+
@classmethod
|
165
|
+
def is_available(cls) -> bool:
|
166
|
+
return sys.platform == 'linux' and shutil.which('lslocks') is not None
|
167
|
+
|
168
|
+
def _read_locking_pid(self, path: str, timeout: Timeout) -> int:
|
169
|
+
lsl_output = LslocksCommand().run(timeout=timeout)
|
170
|
+
|
171
|
+
lsl_pids = {
|
172
|
+
li.pid
|
173
|
+
for li in lsl_output
|
174
|
+
if li.path == path
|
175
|
+
and li.type == 'FLOCK'
|
176
|
+
}
|
177
|
+
if not lsl_pids:
|
178
|
+
raise self.NoOwnerError
|
179
|
+
if len(lsl_pids) != 1:
|
180
|
+
raise RuntimeError(f'Multiple locks on file: {path}')
|
181
|
+
|
182
|
+
[pid] = lsl_pids
|
183
|
+
return pid
|
184
|
+
|
185
|
+
class _Result(ta.NamedTuple):
|
186
|
+
pid: int
|
187
|
+
pidfd: int
|
188
|
+
|
189
|
+
def _try_read_and_verify(
|
190
|
+
self,
|
191
|
+
pidfile: Pidfile,
|
192
|
+
timeout: Timeout,
|
193
|
+
) -> ta.Optional[_Result]:
|
194
|
+
path = os.path.abspath(pidfile.path)
|
195
|
+
initial_pid = self._read_locking_pid(path, timeout)
|
196
|
+
|
197
|
+
try:
|
198
|
+
pidfd = os.open(f'/proc/{initial_pid}', os.O_RDONLY)
|
199
|
+
except FileNotFoundError:
|
200
|
+
raise self.NoOwnerError from None
|
201
|
+
|
202
|
+
try:
|
203
|
+
reread_pid = self._read_locking_pid(path, timeout)
|
204
|
+
if reread_pid != initial_pid:
|
205
|
+
os.close(pidfd)
|
206
|
+
return None
|
207
|
+
|
208
|
+
return self._Result(initial_pid, pidfd)
|
209
|
+
|
210
|
+
except BaseException:
|
211
|
+
os.close(pidfd)
|
212
|
+
raise
|
213
|
+
|
214
|
+
@contextlib.contextmanager
|
215
|
+
def _pin_pidfile_owner(
|
216
|
+
self,
|
217
|
+
pidfile: Pidfile,
|
218
|
+
timeout: Timeout,
|
219
|
+
) -> ta.Iterator[int]:
|
220
|
+
while (res := self._try_read_and_verify(pidfile, timeout)) is None:
|
221
|
+
time.sleep(self._sleep_s)
|
222
|
+
timeout()
|
223
|
+
|
224
|
+
try:
|
225
|
+
yield res.pid
|
226
|
+
finally:
|
227
|
+
os.close(res.pidfd)
|
228
|
+
|
229
|
+
|
230
|
+
##
|
231
|
+
|
232
|
+
|
233
|
+
if __name__ == '__main__':
|
234
|
+
def _main() -> None:
|
235
|
+
argparse = __import__('argparse')
|
236
|
+
parser = argparse.ArgumentParser()
|
237
|
+
parser.add_argument('file')
|
238
|
+
args = parser.parse_args()
|
239
|
+
|
240
|
+
with PidfilePinner.default_impl()().pin_pidfile_owner(
|
241
|
+
args.file,
|
242
|
+
timeout=5.,
|
243
|
+
) as pid:
|
244
|
+
print(pid)
|
245
|
+
|
246
|
+
_main()
|
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=YGmAnUBszmosQQ_7Hh2wwtDiYdYZ4unNKYzOtALuels,7968
|
2
|
-
omlish/__about__.py,sha256=
|
2
|
+
omlish/__about__.py,sha256=F8eB77QCwYkeNA3ZSyaAdpFyPxSptO9EjeuJvnM2Lrg,3380
|
3
3
|
omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
|
4
4
|
omlish/c3.py,sha256=ubu7lHwss5V4UznbejAI0qXhXahrU01MysuHOZI9C4U,8116
|
5
5
|
omlish/cached.py,sha256=UI-XTFBwA6YXWJJJeBn-WkwBkfzDjLBBaZf4nIJA9y0,510
|
@@ -203,8 +203,8 @@ omlish/dataclasses/impl/utils.py,sha256=aER2iL3UAtgS1BdLuEvTr9Tr2wC28wk1kiOeO-jI
|
|
203
203
|
omlish/diag/__init__.py,sha256=4S8v0myJM4Zld6_FV6cPe_nSv0aJb6kXftEit0HkiGE,1141
|
204
204
|
omlish/diag/asts.py,sha256=BveUUNUcaAm4Hg55f4ZxGSI313E4L8cCZ5XjHpEkKVI,3325
|
205
205
|
omlish/diag/debug.py,sha256=ClED7kKXeVMyKrjGIxcq14kXk9kvUJfytBQwK9y7c4Q,1637
|
206
|
-
omlish/diag/lslocks.py,sha256=
|
207
|
-
omlish/diag/lsof.py,sha256
|
206
|
+
omlish/diag/lslocks.py,sha256=fWI3SZwgEkhipVfSqvzVzREJRShcDYmlYByHBT0LToc,1744
|
207
|
+
omlish/diag/lsof.py,sha256=-s-0QKFC-_vfdkxhk0m_Lq-PVrXCY7TUFV9ff_Q_gdQ,9319
|
208
208
|
omlish/diag/procfs.py,sha256=KaGTAA2Gj8eEEp7MjClRe4aimwzd-HDABThFzvq2cBQ,9684
|
209
209
|
omlish/diag/procstats.py,sha256=UkqxREqfd-38xPYZ9T1SIJISz5ARQCEhTtOZrxtm2dE,777
|
210
210
|
omlish/diag/ps.py,sha256=mwraPO6Nze_2r9RiC4dyLL0aJ_lHR1_fSN9BlYNBoHw,1651
|
@@ -505,8 +505,9 @@ omlish/os/paths.py,sha256=hqPiyg_eYaRoIVPdAeX4oeLEV4Kpln_XsH0tHvbOf8Q,844
|
|
505
505
|
omlish/os/sizes.py,sha256=ohkALLvqSqBX4iR-7DMKJ4pfOCRdZXV8htH4QywUNM0,152
|
506
506
|
omlish/os/temp.py,sha256=P97KiVeNB7rfGn4tlgU5ro86JUxAsiphLMlxsjQgfB0,1198
|
507
507
|
omlish/os/pidfiles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
508
|
-
omlish/os/pidfiles/manager.py,sha256=
|
509
|
-
omlish/os/pidfiles/pidfile.py,sha256=
|
508
|
+
omlish/os/pidfiles/manager.py,sha256=XP_0ynjWDG2wjRN3sIKjmFafpf-er3AYm8E7aXs16fA,3209
|
509
|
+
omlish/os/pidfiles/pidfile.py,sha256=WmZt_c8fvztPgZQnYHhcQCKWgHqAEsaI3Ggz6Wqgkc8,3748
|
510
|
+
omlish/os/pidfiles/pinning.py,sha256=YmlHK4G4t38Uldx-kHQBSMSwCzETevDYBDED9HhCCvQ,6153
|
510
511
|
omlish/reflect/__init__.py,sha256=JBWwxKwP4IEaomkK0PTju02STU1BVXT14SCrShT1Sm0,769
|
511
512
|
omlish/reflect/inspect.py,sha256=veJ424-9oZrqyvhVpvxOi7hcKW-PDBkdYL2yjrFlk4o,495
|
512
513
|
omlish/reflect/ops.py,sha256=RJ6jzrM4ieFsXzWyNXWV43O_WgzEaUvlHSc5N2ezW2A,2044
|
@@ -679,9 +680,9 @@ omlish/text/indent.py,sha256=YjtJEBYWuk8--b9JU_T6q4yxV85_TR7VEVr5ViRCFwk,1336
|
|
679
680
|
omlish/text/minja.py,sha256=jZC-fp3Xuhx48ppqsf2Sf1pHbC0t8XBB7UpUUoOk2Qw,5751
|
680
681
|
omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
|
681
682
|
omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
|
682
|
-
omlish-0.0.0.
|
683
|
-
omlish-0.0.0.
|
684
|
-
omlish-0.0.0.
|
685
|
-
omlish-0.0.0.
|
686
|
-
omlish-0.0.0.
|
687
|
-
omlish-0.0.0.
|
683
|
+
omlish-0.0.0.dev227.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
684
|
+
omlish-0.0.0.dev227.dist-info/METADATA,sha256=Pl1bZYBMsE5uKxtT265EkiV61cZ6y6-u11m6uqsXwz4,4176
|
685
|
+
omlish-0.0.0.dev227.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
686
|
+
omlish-0.0.0.dev227.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
687
|
+
omlish-0.0.0.dev227.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
688
|
+
omlish-0.0.0.dev227.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|