omlish 0.0.0.dev226__py3-none-any.whl → 0.0.0.dev227__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|