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 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,,