omlish 0.0.0.dev225__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/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,
|