omlish 0.0.0.dev225__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/asyncs/asyncio/subprocesses.py +4 -3
- omlish/asyncs/asyncio/timeouts.py +5 -2
- omlish/diag/lslocks.py +64 -0
- omlish/diag/lsof.py +264 -0
- omlish/diag/ps.py +40 -21
- omlish/lang/__init__.py +7 -8
- omlish/lite/dataclasses.py +23 -0
- omlish/lite/timeouts.py +202 -0
- omlish/os/fcntl.py +59 -0
- omlish/os/pidfiles/manager.py +17 -0
- omlish/os/pidfiles/pidfile.py +42 -12
- omlish/os/pidfiles/pinning.py +246 -0
- omlish/subprocesses/async_.py +2 -1
- omlish/subprocesses/base.py +6 -0
- omlish/subprocesses/run.py +48 -8
- omlish/subprocesses/sync.py +7 -1
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/RECORD +23 -19
- omlish/lang/timeouts.py +0 -53
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev225.dist-info → omlish-0.0.0.dev227.dist-info}/top_level.txt +0 -0
omlish/lite/timeouts.py
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
TODO:
|
4
|
+
- Event (/ Predicate)
|
5
|
+
"""
|
6
|
+
import abc
|
7
|
+
import time
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
|
11
|
+
TimeoutLike = ta.Union['Timeout', 'Timeout.Default', ta.Iterable['TimeoutLike'], float] # ta.TypeAlias
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class Timeout(abc.ABC):
|
18
|
+
@property
|
19
|
+
@abc.abstractmethod
|
20
|
+
def can_expire(self) -> bool:
|
21
|
+
"""Indicates whether or not this timeout will ever expire."""
|
22
|
+
|
23
|
+
raise NotImplementedError
|
24
|
+
|
25
|
+
@abc.abstractmethod
|
26
|
+
def expired(self) -> bool:
|
27
|
+
"""Return whether or not this timeout has expired."""
|
28
|
+
|
29
|
+
raise NotImplementedError
|
30
|
+
|
31
|
+
@abc.abstractmethod
|
32
|
+
def remaining(self) -> float:
|
33
|
+
"""Returns the time (in seconds) remaining until the timeout expires. May be negative and/or infinite."""
|
34
|
+
|
35
|
+
raise NotImplementedError
|
36
|
+
|
37
|
+
@abc.abstractmethod
|
38
|
+
def __call__(self) -> float:
|
39
|
+
"""Returns the time (in seconds) remaining until the timeout expires, or raises if the timeout has expired."""
|
40
|
+
|
41
|
+
raise NotImplementedError
|
42
|
+
|
43
|
+
@abc.abstractmethod
|
44
|
+
def or_(self, o: ta.Any) -> ta.Any:
|
45
|
+
"""Evaluates time remaining via remaining() if this timeout can expire, otherwise returns `o`."""
|
46
|
+
|
47
|
+
raise NotImplementedError
|
48
|
+
|
49
|
+
#
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def _now(cls) -> float:
|
53
|
+
return time.time()
|
54
|
+
|
55
|
+
#
|
56
|
+
|
57
|
+
class Default:
|
58
|
+
def __new__(cls, *args, **kwargs): # noqa
|
59
|
+
raise TypeError
|
60
|
+
|
61
|
+
class _NOT_SPECIFIED: # noqa
|
62
|
+
def __new__(cls, *args, **kwargs): # noqa
|
63
|
+
raise TypeError
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def of(
|
67
|
+
cls,
|
68
|
+
obj: ta.Optional[TimeoutLike],
|
69
|
+
default: ta.Union[TimeoutLike, ta.Type[_NOT_SPECIFIED]] = _NOT_SPECIFIED,
|
70
|
+
) -> 'Timeout':
|
71
|
+
if obj is None:
|
72
|
+
return InfiniteTimeout()
|
73
|
+
|
74
|
+
elif isinstance(obj, Timeout):
|
75
|
+
return obj
|
76
|
+
|
77
|
+
elif isinstance(obj, (float, int)):
|
78
|
+
return DeadlineTimeout(cls._now() + obj)
|
79
|
+
|
80
|
+
elif isinstance(obj, ta.Iterable):
|
81
|
+
return CompositeTimeout(*[Timeout.of(c) for c in obj])
|
82
|
+
|
83
|
+
elif obj is Timeout.Default:
|
84
|
+
if default is Timeout._NOT_SPECIFIED or default is Timeout.Default:
|
85
|
+
raise RuntimeError('Must specify a default timeout')
|
86
|
+
|
87
|
+
else:
|
88
|
+
return Timeout.of(default) # type: ignore[arg-type]
|
89
|
+
|
90
|
+
else:
|
91
|
+
raise TypeError(obj)
|
92
|
+
|
93
|
+
@classmethod
|
94
|
+
def of_deadline(cls, deadline: float) -> 'DeadlineTimeout':
|
95
|
+
return DeadlineTimeout(deadline)
|
96
|
+
|
97
|
+
@classmethod
|
98
|
+
def of_predicate(cls, expired_fn: ta.Callable[[], bool]) -> 'PredicateTimeout':
|
99
|
+
return PredicateTimeout(expired_fn)
|
100
|
+
|
101
|
+
|
102
|
+
class DeadlineTimeout(Timeout):
|
103
|
+
def __init__(
|
104
|
+
self,
|
105
|
+
deadline: float,
|
106
|
+
exc: ta.Union[ta.Type[BaseException], BaseException] = TimeoutError,
|
107
|
+
) -> None:
|
108
|
+
super().__init__()
|
109
|
+
|
110
|
+
self.deadline = deadline
|
111
|
+
self.exc = exc
|
112
|
+
|
113
|
+
@property
|
114
|
+
def can_expire(self) -> bool:
|
115
|
+
return True
|
116
|
+
|
117
|
+
def expired(self) -> bool:
|
118
|
+
return not (self.remaining() > 0)
|
119
|
+
|
120
|
+
def remaining(self) -> float:
|
121
|
+
return self.deadline - self._now()
|
122
|
+
|
123
|
+
def __call__(self) -> float:
|
124
|
+
if (rem := self.remaining()) > 0:
|
125
|
+
return rem
|
126
|
+
raise self.exc
|
127
|
+
|
128
|
+
def or_(self, o: ta.Any) -> ta.Any:
|
129
|
+
return self()
|
130
|
+
|
131
|
+
|
132
|
+
class InfiniteTimeout(Timeout):
|
133
|
+
@property
|
134
|
+
def can_expire(self) -> bool:
|
135
|
+
return False
|
136
|
+
|
137
|
+
def expired(self) -> bool:
|
138
|
+
return False
|
139
|
+
|
140
|
+
def remaining(self) -> float:
|
141
|
+
return float('inf')
|
142
|
+
|
143
|
+
def __call__(self) -> float:
|
144
|
+
return float('inf')
|
145
|
+
|
146
|
+
def or_(self, o: ta.Any) -> ta.Any:
|
147
|
+
return o
|
148
|
+
|
149
|
+
|
150
|
+
class CompositeTimeout(Timeout):
|
151
|
+
def __init__(self, *children: Timeout) -> None:
|
152
|
+
super().__init__()
|
153
|
+
|
154
|
+
self.children = children
|
155
|
+
|
156
|
+
@property
|
157
|
+
def can_expire(self) -> bool:
|
158
|
+
return any(c.can_expire for c in self.children)
|
159
|
+
|
160
|
+
def expired(self) -> bool:
|
161
|
+
return any(c.expired() for c in self.children)
|
162
|
+
|
163
|
+
def remaining(self) -> float:
|
164
|
+
return min(c.remaining() for c in self.children)
|
165
|
+
|
166
|
+
def __call__(self) -> float:
|
167
|
+
return min(c() for c in self.children)
|
168
|
+
|
169
|
+
def or_(self, o: ta.Any) -> ta.Any:
|
170
|
+
if self.can_expire:
|
171
|
+
return self()
|
172
|
+
return o
|
173
|
+
|
174
|
+
|
175
|
+
class PredicateTimeout(Timeout):
|
176
|
+
def __init__(
|
177
|
+
self,
|
178
|
+
expired_fn: ta.Callable[[], bool],
|
179
|
+
exc: ta.Union[ta.Type[BaseException], BaseException] = TimeoutError,
|
180
|
+
) -> None:
|
181
|
+
super().__init__()
|
182
|
+
|
183
|
+
self.expired_fn = expired_fn
|
184
|
+
self.exc = exc
|
185
|
+
|
186
|
+
@property
|
187
|
+
def can_expire(self) -> bool:
|
188
|
+
return True
|
189
|
+
|
190
|
+
def expired(self) -> bool:
|
191
|
+
return self.expired_fn()
|
192
|
+
|
193
|
+
def remaining(self) -> float:
|
194
|
+
return float('inf')
|
195
|
+
|
196
|
+
def __call__(self) -> float:
|
197
|
+
if not self.expired_fn():
|
198
|
+
return float('inf')
|
199
|
+
raise self.exc
|
200
|
+
|
201
|
+
def or_(self, o: ta.Any) -> ta.Any:
|
202
|
+
return self()
|
omlish/os/fcntl.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import dataclasses as dc
|
4
|
+
import os
|
5
|
+
import struct
|
6
|
+
import sys
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
|
10
|
+
@dc.dataclass(frozen=True)
|
11
|
+
class FcntlLockData:
|
12
|
+
# cmd = {F_SETLK, F_SETLKW, F_GETLK}
|
13
|
+
|
14
|
+
type: int # {F_RDLCK, F_WRLCK, F_UNLCK}
|
15
|
+
whence: int = os.SEEK_SET
|
16
|
+
start: int = 0
|
17
|
+
len: int = 0
|
18
|
+
pid: int = 0
|
19
|
+
|
20
|
+
#
|
21
|
+
|
22
|
+
_STRUCT_PACK_BY_PLATFORM: ta.ClassVar[ta.Mapping[str, ta.Sequence[ta.Tuple[str, str]]]] = {
|
23
|
+
'linux': [
|
24
|
+
('type', 'h'),
|
25
|
+
('whence', 'h'),
|
26
|
+
('start', 'q'),
|
27
|
+
('len', 'q'),
|
28
|
+
('pid', 'i'),
|
29
|
+
],
|
30
|
+
'darwin': [
|
31
|
+
('start', 'q'),
|
32
|
+
('len', 'q'),
|
33
|
+
('pid', 'i'),
|
34
|
+
('type', 'h'),
|
35
|
+
('whence', 'h'),
|
36
|
+
],
|
37
|
+
}
|
38
|
+
|
39
|
+
def pack(self) -> bytes:
|
40
|
+
try:
|
41
|
+
pack = self._STRUCT_PACK_BY_PLATFORM[sys.platform]
|
42
|
+
except KeyError:
|
43
|
+
raise OSError from None
|
44
|
+
|
45
|
+
fmt = ''.join(f for _, f in pack)
|
46
|
+
tup = [getattr(self, a) for a, _ in pack]
|
47
|
+
return struct.pack(fmt, *tup)
|
48
|
+
|
49
|
+
@classmethod
|
50
|
+
def unpack(cls, data: bytes) -> 'FcntlLockData':
|
51
|
+
try:
|
52
|
+
pack = cls._STRUCT_PACK_BY_PLATFORM[sys.platform]
|
53
|
+
except KeyError:
|
54
|
+
raise OSError from None
|
55
|
+
|
56
|
+
fmt = ''.join(f for _, f in pack)
|
57
|
+
tup = struct.unpack(fmt, data)
|
58
|
+
kw = {a: v for (a, _), v in zip(pack, tup)}
|
59
|
+
return FcntlLockData(**kw)
|
omlish/os/pidfiles/manager.py
CHANGED
@@ -14,6 +14,17 @@ from .pidfile import Pidfile
|
|
14
14
|
|
15
15
|
|
16
16
|
class _PidfileManager:
|
17
|
+
"""
|
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).
|
22
|
+
|
23
|
+
Not implemented as an instantiated class as there is no way to unregister at_fork listeners, and because Pidfiles
|
24
|
+
may be pickled and there must be no possibility of accidentally unpickling and instantiating a new instance of the
|
25
|
+
manager.
|
26
|
+
"""
|
27
|
+
|
17
28
|
def __new__(cls, *args, **kwargs): # noqa
|
18
29
|
raise TypeError
|
19
30
|
|
@@ -71,6 +82,11 @@ class _PidfileManager:
|
|
71
82
|
inheritable: bool = True,
|
72
83
|
**kwargs: ta.Any,
|
73
84
|
) -> ta.Iterator[Pidfile]:
|
85
|
+
"""
|
86
|
+
A contextmanager for creating and managing a Pidfile which will only be inherited by forks of the calling /
|
87
|
+
creating thread.
|
88
|
+
"""
|
89
|
+
|
74
90
|
check.arg(inheritable)
|
75
91
|
|
76
92
|
cls.install()
|
@@ -84,6 +100,7 @@ class _PidfileManager:
|
|
84
100
|
with cls._lock:
|
85
101
|
cls._pidfile_threads[pf] = threading.current_thread()
|
86
102
|
try:
|
103
|
+
|
87
104
|
with pf:
|
88
105
|
os.set_inheritable(check.not_none(pf.fileno()), True)
|
89
106
|
yield pf
|
omlish/os/pidfiles/pidfile.py
CHANGED
@@ -10,6 +10,7 @@ TODO:
|
|
10
10
|
- 3) recheck current pid of flock holder == that pid
|
11
11
|
- racy as to if it's a different actual process as initial check, just with same pid, but due to 'identity' / semantic
|
12
12
|
meaning of the named pidfile the processes are considered equivalent
|
13
|
+
- read_checked(), contextmanager
|
13
14
|
"""
|
14
15
|
import fcntl
|
15
16
|
import os
|
@@ -32,6 +33,14 @@ class Pidfile:
|
|
32
33
|
self._path = path
|
33
34
|
self._inheritable = inheritable
|
34
35
|
|
36
|
+
@property
|
37
|
+
def path(self) -> str:
|
38
|
+
return self._path
|
39
|
+
|
40
|
+
@property
|
41
|
+
def inheritable(self) -> bool:
|
42
|
+
return self._inheritable
|
43
|
+
|
35
44
|
def __repr__(self) -> str:
|
36
45
|
return f'{self.__class__.__name__}({self._path!r})'
|
37
46
|
|
@@ -75,6 +84,7 @@ class Pidfile:
|
|
75
84
|
state = self.__dict__.copy()
|
76
85
|
|
77
86
|
if '_f' in state:
|
87
|
+
# self._inheritable may be decoupled from actual file inheritability - for example when using the manager.
|
78
88
|
if os.get_inheritable(fd := state.pop('_f').fileno()):
|
79
89
|
state['__fd'] = fd
|
80
90
|
|
@@ -99,7 +109,7 @@ class Pidfile:
|
|
99
109
|
del self._f
|
100
110
|
return True
|
101
111
|
|
102
|
-
def
|
112
|
+
def try_acquire_lock(self) -> bool:
|
103
113
|
try:
|
104
114
|
fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
105
115
|
return True
|
@@ -107,14 +117,29 @@ class Pidfile:
|
|
107
117
|
except OSError:
|
108
118
|
return False
|
109
119
|
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
113
138
|
|
114
139
|
#
|
115
140
|
|
116
141
|
def write(self, pid: ta.Optional[int] = None) -> None:
|
117
|
-
self.
|
142
|
+
self.acquire_lock()
|
118
143
|
|
119
144
|
if pid is None:
|
120
145
|
pid = os.getpid()
|
@@ -125,18 +150,23 @@ class Pidfile:
|
|
125
150
|
self._f.flush()
|
126
151
|
|
127
152
|
def clear(self) -> None:
|
128
|
-
self.
|
153
|
+
self.acquire_lock()
|
129
154
|
|
130
155
|
self._f.seek(0)
|
131
156
|
self._f.truncate()
|
132
157
|
|
133
|
-
|
134
|
-
|
135
|
-
|
158
|
+
#
|
159
|
+
|
160
|
+
def read(self) -> ta.Optional[int]:
|
161
|
+
self.ensure_cannot_lock()
|
136
162
|
|
137
163
|
self._f.seek(0)
|
138
|
-
|
164
|
+
buf = self._f.read()
|
165
|
+
if not buf:
|
166
|
+
return None
|
167
|
+
return int(buf)
|
139
168
|
|
140
169
|
def kill(self, sig: int = signal.SIGTERM) -> None:
|
141
|
-
pid
|
142
|
-
|
170
|
+
if (pid := self.read()) is None:
|
171
|
+
raise self.Error(f'Pidfile locked but empty')
|
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()
|
omlish/subprocesses/async_.py
CHANGED
@@ -4,6 +4,7 @@ import abc
|
|
4
4
|
import sys
|
5
5
|
import typing as ta
|
6
6
|
|
7
|
+
from ..lite.timeouts import TimeoutLike
|
7
8
|
from .base import BaseSubprocesses
|
8
9
|
from .run import SubprocessRun
|
9
10
|
from .run import SubprocessRunOutput
|
@@ -21,7 +22,7 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
21
22
|
self,
|
22
23
|
*cmd: str,
|
23
24
|
input: ta.Any = None, # noqa
|
24
|
-
timeout: ta.Optional[
|
25
|
+
timeout: ta.Optional[TimeoutLike] = None,
|
25
26
|
check: bool = False,
|
26
27
|
capture_output: ta.Optional[bool] = None,
|
27
28
|
**kwargs: ta.Any,
|
omlish/subprocesses/base.py
CHANGED
@@ -8,6 +8,7 @@ import subprocess
|
|
8
8
|
import time
|
9
9
|
import typing as ta
|
10
10
|
|
11
|
+
from ..lite.timeouts import Timeout
|
11
12
|
from .wrap import subprocess_maybe_shell_wrap_exec
|
12
13
|
|
13
14
|
|
@@ -111,6 +112,11 @@ class BaseSubprocesses(abc.ABC): # noqa
|
|
111
112
|
|
112
113
|
#
|
113
114
|
|
115
|
+
if 'timeout' in kwargs:
|
116
|
+
kwargs['timeout'] = Timeout.of(kwargs['timeout']).or_(None)
|
117
|
+
|
118
|
+
#
|
119
|
+
|
114
120
|
return cmd, dict(
|
115
121
|
env=env,
|
116
122
|
shell=shell,
|