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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev226'
2
- __revision__ = '1f3419dac98e9db37df48a9a952f780d17bf31ed'
1
+ __version__ = '0.0.0.dev227'
2
+ __revision__ = '2faa3e7f9120edd3e6dd79fad6401fd87d89f5cf'
3
3
 
4
4
 
5
5
  #
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 LsLocksItem:
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 LsLocksCommand(SubprocessRunnable):
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[LsLocksItem]:
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[LsLocksItem])
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 LsofCommand(SubprocessRunnable[ta.List[LsofItem]]):
218
+ class \
219
+ LsofCommand(SubprocessRunnable[ta.List[LsofItem]]):
220
220
  pid: ta.Optional[int] = None
221
221
  file: ta.Optional[str] = None
222
222
 
@@ -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
@@ -109,7 +109,7 @@ class Pidfile:
109
109
  del self._f
110
110
  return True
111
111
 
112
- def try_lock(self) -> bool:
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
- def ensure_locked(self) -> None:
121
- if not self.try_lock():
122
- raise RuntimeError('Could not get lock')
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.ensure_locked()
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.ensure_locked()
153
+ self.acquire_lock()
139
154
 
140
155
  self._f.seek(0)
141
156
  self._f.truncate()
142
157
 
143
- def read(self) -> int:
144
- if self.try_lock():
145
- raise RuntimeError('Got lock')
158
+ #
159
+
160
+ def read(self) -> ta.Optional[int]:
161
+ self.ensure_cannot_lock()
146
162
 
147
163
  self._f.seek(0)
148
- return int(self._f.read())
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 = self.read()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: omlish
3
- Version: 0.0.0.dev226
3
+ Version: 0.0.0.dev227
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=YGmAnUBszmosQQ_7Hh2wwtDiYdYZ4unNKYzOtALuels,7968
2
- omlish/__about__.py,sha256=QR1QmjxqAJ538XauJdEuHH505rJ9QF67p3blNgi6kwc,3380
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=7I_u-Vx0Dz0KN59Y6dk9nBZynHa6usvKIXVSywGCiWE,1722
207
- omlish/diag/lsof.py,sha256=gMtNmfBo1AlYmvZWBB51u4NfbfC6yJyEVwfr1HgktP8,9328
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=ssnxCvSoL3OapzcMmxSgiy9o1NogR6PTX4LLTJXuPkM,2830
509
- omlish/os/pidfiles/pidfile.py,sha256=833aPCV7cQqFqYnxwma8np0eoy0CJzVHRskn-YSQwis,3378
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.dev226.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
683
- omlish-0.0.0.dev226.dist-info/METADATA,sha256=0HcF368bfjlzkGzprdbRX4GJagL5c9PDPRUqiBYWIlY,4176
684
- omlish-0.0.0.dev226.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
685
- omlish-0.0.0.dev226.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
686
- omlish-0.0.0.dev226.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
687
- omlish-0.0.0.dev226.dist-info/RECORD,,
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,,