omlish 0.0.0.dev226__py3-none-any.whl → 0.0.0.dev228__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.
Files changed (50) hide show
  1. omlish/__about__.py +3 -3
  2. omlish/diag/lslocks.py +4 -4
  3. omlish/diag/lsof.py +3 -4
  4. omlish/diag/ps.py +9 -0
  5. omlish/lite/timeouts.py +1 -1
  6. omlish/marshal/__init__.py +39 -24
  7. omlish/marshal/composite/__init__.py +0 -0
  8. omlish/marshal/{iterables.py → composite/iterables.py} +10 -10
  9. omlish/marshal/{literals.py → composite/literals.py} +9 -9
  10. omlish/marshal/{mappings.py → composite/mappings.py} +10 -10
  11. omlish/marshal/{maybes.py → composite/maybes.py} +11 -11
  12. omlish/marshal/{newtypes.py → composite/newtypes.py} +8 -8
  13. omlish/marshal/{optionals.py → composite/optionals.py} +9 -9
  14. omlish/marshal/objects/__init__.py +7 -0
  15. omlish/marshal/{dataclasses.py → objects/dataclasses.py} +24 -24
  16. omlish/marshal/{helpers.py → objects/helpers.py} +6 -3
  17. omlish/marshal/objects/marshal.py +108 -0
  18. omlish/marshal/objects/metadata.py +124 -0
  19. omlish/marshal/{namedtuples.py → objects/namedtuples.py} +16 -16
  20. omlish/marshal/objects/unmarshal.py +141 -0
  21. omlish/marshal/polymorphism/__init__.py +7 -0
  22. omlish/marshal/polymorphism/marshal.py +66 -0
  23. omlish/marshal/polymorphism/metadata.py +140 -0
  24. omlish/marshal/{unions.py → polymorphism/unions.py} +18 -18
  25. omlish/marshal/polymorphism/unmarshal.py +73 -0
  26. omlish/marshal/singular/__init__.py +0 -0
  27. omlish/marshal/{any.py → singular/any.py} +8 -8
  28. omlish/marshal/{base64.py → singular/base64.py} +9 -9
  29. omlish/marshal/{datetimes.py → singular/datetimes.py} +9 -9
  30. omlish/marshal/{enums.py → singular/enums.py} +9 -9
  31. omlish/marshal/{numbers.py → singular/numbers.py} +8 -8
  32. omlish/marshal/{primitives.py → singular/primitives.py} +8 -8
  33. omlish/marshal/{uuids.py → singular/uuids.py} +8 -8
  34. omlish/marshal/standard.py +32 -32
  35. omlish/os/death.py +70 -4
  36. omlish/os/fcntl.py +11 -12
  37. omlish/os/files.py +18 -3
  38. omlish/os/forkhooks.py +215 -0
  39. omlish/os/pidfiles/manager.py +4 -1
  40. omlish/os/pidfiles/pidfile.py +31 -11
  41. omlish/os/pidfiles/pinning.py +250 -0
  42. omlish/sockets/bind.py +4 -4
  43. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/METADATA +3 -3
  44. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/RECORD +48 -38
  45. omlish/marshal/objects.py +0 -317
  46. omlish/marshal/polymorphism.py +0 -267
  47. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/LICENSE +0 -0
  48. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/WHEEL +0 -0
  49. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/entry_points.txt +0 -0
  50. {omlish-0.0.0.dev226.dist-info → omlish-0.0.0.dev228.dist-info}/top_level.txt +0 -0
@@ -2,15 +2,15 @@ import dataclasses as dc
2
2
  import datetime
3
3
  import typing as ta
4
4
 
5
- from .. import check
6
- from .. import datetimes as dts
7
- from .base import MarshalContext
8
- from .base import Marshaler
9
- from .base import TypeMapMarshalerFactory
10
- from .base import TypeMapUnmarshalerFactory
11
- from .base import UnmarshalContext
12
- from .base import Unmarshaler
13
- from .values import Value
5
+ from ... import check
6
+ from ... import datetimes as dts
7
+ from ..base import MarshalContext
8
+ from ..base import Marshaler
9
+ from ..base import TypeMapMarshalerFactory
10
+ from ..base import TypeMapUnmarshalerFactory
11
+ from ..base import UnmarshalContext
12
+ from ..base import Unmarshaler
13
+ from ..values import Value
14
14
 
15
15
 
16
16
  DatetimeLikeT = ta.TypeVar('DatetimeLikeT', bound=datetime.datetime | datetime.date | datetime.time)
@@ -2,15 +2,15 @@ import dataclasses as dc
2
2
  import enum
3
3
  import typing as ta
4
4
 
5
- from .. import check
6
- from .. import reflect as rfl
7
- from .base import MarshalContext
8
- from .base import Marshaler
9
- from .base import MarshalerFactory
10
- from .base import UnmarshalContext
11
- from .base import Unmarshaler
12
- from .base import UnmarshalerFactory
13
- from .values import Value
5
+ from ... import check
6
+ from ... import reflect as rfl
7
+ from ..base import MarshalContext
8
+ from ..base import Marshaler
9
+ from ..base import MarshalerFactory
10
+ from ..base import UnmarshalContext
11
+ from ..base import Unmarshaler
12
+ from ..base import UnmarshalerFactory
13
+ from ..values import Value
14
14
 
15
15
 
16
16
  @dc.dataclass(frozen=True)
@@ -2,14 +2,14 @@ import decimal
2
2
  import fractions
3
3
  import typing as ta
4
4
 
5
- from .. import check
6
- from .base import MarshalContext
7
- from .base import Marshaler
8
- from .base import TypeMapMarshalerFactory
9
- from .base import TypeMapUnmarshalerFactory
10
- from .base import UnmarshalContext
11
- from .base import Unmarshaler
12
- from .values import Value
5
+ from ... import check
6
+ from ..base import MarshalContext
7
+ from ..base import Marshaler
8
+ from ..base import TypeMapMarshalerFactory
9
+ from ..base import TypeMapUnmarshalerFactory
10
+ from ..base import UnmarshalContext
11
+ from ..base import Unmarshaler
12
+ from ..values import Value
13
13
 
14
14
 
15
15
  class ComplexMarshalerUnmarshaler(Marshaler, Unmarshaler):
@@ -4,14 +4,14 @@ TODO:
4
4
  """
5
5
  import typing as ta
6
6
 
7
- from .. import dataclasses as dc
8
- from .base import MarshalContext
9
- from .base import Marshaler
10
- from .base import TypeMapMarshalerFactory
11
- from .base import TypeMapUnmarshalerFactory
12
- from .base import UnmarshalContext
13
- from .base import Unmarshaler
14
- from .values import Value
7
+ from ... import dataclasses as dc
8
+ from ..base import MarshalContext
9
+ from ..base import Marshaler
10
+ from ..base import TypeMapMarshalerFactory
11
+ from ..base import TypeMapUnmarshalerFactory
12
+ from ..base import UnmarshalContext
13
+ from ..base import Unmarshaler
14
+ from ..values import Value
15
15
 
16
16
 
17
17
  ##
@@ -1,14 +1,14 @@
1
1
  import re
2
2
  import uuid
3
3
 
4
- from .. import check
5
- from .base import MarshalContext
6
- from .base import Marshaler
7
- from .base import TypeMapMarshalerFactory
8
- from .base import TypeMapUnmarshalerFactory
9
- from .base import UnmarshalContext
10
- from .base import Unmarshaler
11
- from .values import Value
4
+ from ... import check
5
+ from ..base import MarshalContext
6
+ from ..base import Marshaler
7
+ from ..base import TypeMapMarshalerFactory
8
+ from ..base import TypeMapUnmarshalerFactory
9
+ from ..base import UnmarshalContext
10
+ from ..base import Unmarshaler
11
+ from ..values import Value
12
12
 
13
13
 
14
14
  PATTERN = re.compile(r'([0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12})|([0-9A-Fa-f]{32})')
@@ -1,42 +1,42 @@
1
1
  from ..funcs import match as mfs
2
- from .any import ANY_MARSHALER_FACTORY
3
- from .any import ANY_UNMARSHALER_FACTORY
4
2
  from .base import MarshalerFactory
5
3
  from .base import RecursiveMarshalerFactory
6
4
  from .base import RecursiveUnmarshalerFactory
7
5
  from .base import TypeCacheMarshalerFactory
8
6
  from .base import TypeCacheUnmarshalerFactory
9
7
  from .base import UnmarshalerFactory
10
- from .base64 import BASE64_MARSHALER_FACTORY
11
- from .base64 import BASE64_UNMARSHALER_FACTORY
12
- from .dataclasses import DataclassMarshalerFactory
13
- from .dataclasses import DataclassUnmarshalerFactory
14
- from .datetimes import DATETIME_MARSHALER_FACTORY
15
- from .datetimes import DATETIME_UNMARSHALER_FACTORY
16
- from .enums import EnumMarshalerFactory
17
- from .enums import EnumUnmarshalerFactory
18
- from .iterables import IterableMarshalerFactory
19
- from .iterables import IterableUnmarshalerFactory
20
- from .literals import LiteralMarshalerFactory
21
- from .literals import LiteralUnmarshalerFactory
22
- from .mappings import MappingMarshalerFactory
23
- from .mappings import MappingUnmarshalerFactory
24
- from .maybes import MaybeMarshalerFactory
25
- from .maybes import MaybeUnmarshalerFactory
26
- from .namedtuples import NamedtupleMarshalerFactory
27
- from .namedtuples import NamedtupleUnmarshalerFactory
28
- from .newtypes import NewtypeMarshalerFactory
29
- from .newtypes import NewtypeUnmarshalerFactory
30
- from .numbers import NUMBERS_MARSHALER_FACTORY
31
- from .numbers import NUMBERS_UNMARSHALER_FACTORY
32
- from .optionals import OptionalMarshalerFactory
33
- from .optionals import OptionalUnmarshalerFactory
34
- from .primitives import PRIMITIVE_MARSHALER_FACTORY
35
- from .primitives import PRIMITIVE_UNMARSHALER_FACTORY
36
- from .unions import PrimitiveUnionMarshalerFactory
37
- from .unions import PrimitiveUnionUnmarshalerFactory
38
- from .uuids import UUID_MARSHALER_FACTORY
39
- from .uuids import UUID_UNMARSHALER_FACTORY
8
+ from .composite.iterables import IterableMarshalerFactory
9
+ from .composite.iterables import IterableUnmarshalerFactory
10
+ from .composite.literals import LiteralMarshalerFactory
11
+ from .composite.literals import LiteralUnmarshalerFactory
12
+ from .composite.mappings import MappingMarshalerFactory
13
+ from .composite.mappings import MappingUnmarshalerFactory
14
+ from .composite.maybes import MaybeMarshalerFactory
15
+ from .composite.maybes import MaybeUnmarshalerFactory
16
+ from .composite.newtypes import NewtypeMarshalerFactory
17
+ from .composite.newtypes import NewtypeUnmarshalerFactory
18
+ from .composite.optionals import OptionalMarshalerFactory
19
+ from .composite.optionals import OptionalUnmarshalerFactory
20
+ from .objects.dataclasses import DataclassMarshalerFactory
21
+ from .objects.dataclasses import DataclassUnmarshalerFactory
22
+ from .objects.namedtuples import NamedtupleMarshalerFactory
23
+ from .objects.namedtuples import NamedtupleUnmarshalerFactory
24
+ from .polymorphism.unions import PrimitiveUnionMarshalerFactory
25
+ from .polymorphism.unions import PrimitiveUnionUnmarshalerFactory
26
+ from .singular.any import ANY_MARSHALER_FACTORY
27
+ from .singular.any import ANY_UNMARSHALER_FACTORY
28
+ from .singular.base64 import BASE64_MARSHALER_FACTORY
29
+ from .singular.base64 import BASE64_UNMARSHALER_FACTORY
30
+ from .singular.datetimes import DATETIME_MARSHALER_FACTORY
31
+ from .singular.datetimes import DATETIME_UNMARSHALER_FACTORY
32
+ from .singular.enums import EnumMarshalerFactory
33
+ from .singular.enums import EnumUnmarshalerFactory
34
+ from .singular.numbers import NUMBERS_MARSHALER_FACTORY
35
+ from .singular.numbers import NUMBERS_UNMARSHALER_FACTORY
36
+ from .singular.primitives import PRIMITIVE_MARSHALER_FACTORY
37
+ from .singular.primitives import PRIMITIVE_UNMARSHALER_FACTORY
38
+ from .singular.uuids import UUID_MARSHALER_FACTORY
39
+ from .singular.uuids import UUID_UNMARSHALER_FACTORY
40
40
 
41
41
 
42
42
  ##
omlish/os/death.py CHANGED
@@ -4,8 +4,11 @@ import signal
4
4
  import sys
5
5
  import time
6
6
  import typing as ta
7
+ import weakref
7
8
 
8
9
  from .. import check
10
+ from .forkhooks import ForkHook
11
+ from .forkhooks import get_fork_depth
9
12
 
10
13
 
11
14
  ##
@@ -82,12 +85,24 @@ class BaseDeathpact(Deathpact, abc.ABC):
82
85
 
83
86
 
84
87
  class PipeDeathpact(BaseDeathpact):
88
+ """
89
+ NOTE: Closes write side in children lazily on poll - does not proactively close write sides on fork. This means
90
+ parents which fork children into codepaths unaware of live PipeDeathpacts will leave write sides open in those
91
+ children, potentially leading to zombies (if those children outlast the parent). Use ForkAwarePipeDeathpact to
92
+ handle such cases.
93
+ """
94
+
95
+ _COOKIE: ta.ClassVar[bytes] = os.urandom(16)
96
+
85
97
  def __init__(self, **kwargs: ta.Any) -> None:
86
98
  super().__init__(**kwargs)
87
99
 
88
100
  self._rfd: int | None = None
89
101
  self._wfd: int | None = None
90
102
 
103
+ self._cookie: bytes | None = self._COOKIE
104
+ self._fork_depth: int | None = get_fork_depth()
105
+
91
106
  def __repr__(self) -> str:
92
107
  return f'{self.__class__.__name__}(rfd={self._rfd}, wfd={self._wfd})'
93
108
 
@@ -95,26 +110,52 @@ class PipeDeathpact(BaseDeathpact):
95
110
  def pass_fd(self) -> int:
96
111
  return check.not_none(self._rfd)
97
112
 
113
+ def is_parent(self) -> bool:
114
+ return (self._COOKIE, get_fork_depth()) == (self._cookie, self._fork_depth)
115
+
116
+ #
117
+
98
118
  def __enter__(self) -> ta.Self:
99
119
  check.none(self._rfd)
100
120
  check.none(self._wfd)
101
121
 
102
122
  self._rfd, self._wfd = os.pipe()
103
123
 
104
- os.set_inheritable(self._rfd, True)
105
124
  os.set_blocking(self._rfd, False)
106
125
 
107
126
  return self
108
127
 
109
- def __exit__(self, exc_type, exc_val, exc_tb):
110
- if self._rfd is not None:
111
- os.close(check.not_none(self._wfd))
128
+ def _close_wfd_if_not_parent(self) -> None:
129
+ if self._wfd is not None:
130
+ if not self.is_parent():
131
+ os.close(check.not_none(self._wfd))
112
132
  self._wfd = None
113
133
 
134
+ def __exit__(self, exc_type, exc_val, exc_tb):
135
+ if self._rfd is not None:
114
136
  os.close(self._rfd)
115
137
  self._rfd = None
116
138
 
139
+ self._close_wfd_if_not_parent()
140
+
141
+ #
142
+
143
+ def __getstate__(self):
144
+ return dict(
145
+ **self.__dict__,
146
+ _wfd=None,
147
+ _cookie=None,
148
+ _fork_depth=None,
149
+ )
150
+
151
+ def __setstate__(self, state):
152
+ self.__dict__.update(state)
153
+
154
+ #
155
+
117
156
  def should_die(self) -> bool:
157
+ self._close_wfd_if_not_parent()
158
+
118
159
  try:
119
160
  buf = os.read(check.not_none(self._rfd), 1)
120
161
  except BlockingIOError:
@@ -125,3 +166,28 @@ class PipeDeathpact(BaseDeathpact):
125
166
  self.die()
126
167
 
127
168
  return True
169
+
170
+
171
+ #
172
+
173
+
174
+ class ForkAwarePipeDeathpact(PipeDeathpact):
175
+ """
176
+ TODO:
177
+ - Despite no correct way to do threads+forks, still audit thread-safety. Is WeakSet threadsafe? Probably not..
178
+ """
179
+
180
+ _PARENTS: ta.ClassVar[ta.MutableSet['ForkAwarePipeDeathpact']] = weakref.WeakSet()
181
+
182
+ def __init__(self, **kwargs: ta.Any) -> None:
183
+ super().__init__(**kwargs)
184
+
185
+ self._ForkHook.install()
186
+ self._PARENTS.add(self)
187
+
188
+ class _ForkHook(ForkHook):
189
+ @classmethod
190
+ def _after_fork_in_child(cls) -> None:
191
+ for pdp in ForkAwarePipeDeathpact._PARENTS:
192
+ pdp._close_wfd_if_not_parent() # noqa
193
+ ForkAwarePipeDeathpact._PARENTS.clear()
omlish/os/fcntl.py CHANGED
@@ -19,7 +19,7 @@ class FcntlLockData:
19
19
 
20
20
  #
21
21
 
22
- _STRUCT_PACK_BY_PLATFORM: ta.ClassVar[ta.Mapping[str, ta.Sequence[ta.Tuple[str, str]]]] = {
22
+ _STRUCT_PACKING_BY_PLATFORM: ta.ClassVar[ta.Mapping[str, ta.Sequence[ta.Tuple[str, str]]]] = {
23
23
  'linux': [
24
24
  ('type', 'h'),
25
25
  ('whence', 'h'),
@@ -36,24 +36,23 @@ class FcntlLockData:
36
36
  ],
37
37
  }
38
38
 
39
- def pack(self) -> bytes:
39
+ @classmethod
40
+ def _struct_packing(cls) -> ta.Sequence[ta.Tuple[str, str]]:
40
41
  try:
41
- pack = self._STRUCT_PACK_BY_PLATFORM[sys.platform]
42
+ return cls._STRUCT_PACKING_BY_PLATFORM[sys.platform]
42
43
  except KeyError:
43
44
  raise OSError from None
44
45
 
45
- fmt = ''.join(f for _, f in pack)
46
- tup = [getattr(self, a) for a, _ in pack]
46
+ def pack(self) -> bytes:
47
+ packing = self._struct_packing()
48
+ fmt = ''.join(f for _, f in packing)
49
+ tup = [getattr(self, a) for a, _ in packing]
47
50
  return struct.pack(fmt, *tup)
48
51
 
49
52
  @classmethod
50
53
  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)
54
+ packing = cls._struct_packing()
55
+ fmt = ''.join(f for _, f in packing)
57
56
  tup = struct.unpack(fmt, data)
58
- kw = {a: v for (a, _), v in zip(pack, tup)}
57
+ kw = {a: v for (a, _), v in zip(packing, tup)}
59
58
  return FcntlLockData(**kw)
omlish/os/files.py CHANGED
@@ -1,24 +1,39 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
3
  import contextlib
4
+ import errno
5
+ import fcntl
4
6
  import os
5
7
  import typing as ta
6
8
 
7
9
 
8
- def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
10
+ def is_fd_open(fd: int) -> bool:
11
+ try:
12
+ fcntl.fcntl(fd, fcntl.F_GETFD)
13
+ except OSError as e:
14
+ if e.errno == errno.EBADF:
15
+ return False
16
+ raise
17
+ else:
18
+ return True
19
+
20
+
21
+ def touch(path: str, mode: int = 0o666, exist_ok: bool = True) -> None:
9
22
  if exist_ok:
10
23
  # First try to bump modification time
11
24
  # Implementation note: GNU touch uses the UTIME_NOW option of the utimensat() / futimens() functions.
12
25
  try:
13
- os.utime(self, None)
26
+ os.utime(path, None)
14
27
  except OSError:
15
28
  pass
16
29
  else:
17
30
  return
31
+
18
32
  flags = os.O_CREAT | os.O_WRONLY
19
33
  if not exist_ok:
20
34
  flags |= os.O_EXCL
21
- fd = os.open(self, flags, mode)
35
+
36
+ fd = os.open(path, flags, mode)
22
37
  os.close(fd)
23
38
 
24
39
 
omlish/os/forkhooks.py ADDED
@@ -0,0 +1,215 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - ForkHook base class? all classmethods? prevents pickling
6
+ """
7
+ import abc
8
+ import os
9
+ import threading
10
+ import typing as ta
11
+
12
+ from ..lite.check import check
13
+
14
+
15
+ ##
16
+
17
+
18
+ class _ForkHookManager:
19
+ def __new__(cls, *args, **kwargs): # noqa
20
+ raise TypeError
21
+
22
+ def __init_subclass__(cls, **kwargs): # noqa
23
+ raise TypeError
24
+
25
+ #
26
+
27
+ # Intentionally not an RLock - do not nest calls.
28
+ _lock: ta.ClassVar[threading.Lock] = threading.Lock()
29
+
30
+ #
31
+
32
+ _installed: ta.ClassVar[bool] = False
33
+
34
+ @classmethod
35
+ def _install(cls) -> bool:
36
+ if cls._installed:
37
+ return False
38
+
39
+ check.state(not cls._installed)
40
+
41
+ os.register_at_fork(
42
+ before=cls._before_fork,
43
+ after_in_parent=cls._after_fork_in_parent,
44
+ after_in_child=cls._after_fork_in_child,
45
+ )
46
+
47
+ cls._installed = True
48
+ return True
49
+
50
+ @classmethod
51
+ def install(cls) -> bool:
52
+ with cls._lock:
53
+ return cls._install()
54
+
55
+ #
56
+
57
+ class Hook(ta.NamedTuple):
58
+ key: ta.Any
59
+ priority: int
60
+
61
+ # NOTE: these are called inside the global, non-reentrant manager lock
62
+ before_fork: ta.Optional[ta.Callable[[], None]] = None
63
+ after_fork_in_parent: ta.Optional[ta.Callable[[], None]] = None
64
+ after_fork_in_child: ta.Optional[ta.Callable[[], None]] = None
65
+
66
+ #
67
+
68
+ _hooks_by_key: ta.ClassVar[ta.Dict[str, Hook]] = {}
69
+
70
+ _hook_keys: ta.ClassVar[ta.FrozenSet[str]] = frozenset()
71
+ _priority_ordered_hooks: ta.ClassVar[ta.List[Hook]] = []
72
+
73
+ @classmethod
74
+ def _rebuild_hook_collections(cls) -> None:
75
+ cls._hook_keys = frozenset(cls._hooks_by_key)
76
+
77
+ # Uses on dict order preservation for insertion-order of hooks of equal priority (although that shouldn't be
78
+ # depended upon for usecase correctness.
79
+ cls._priority_ordered_hooks = sorted(cls._hooks_by_key.values(), key=lambda h: h.priority)
80
+
81
+ #
82
+
83
+ class HookAlreadyPresentError(Exception):
84
+ pass
85
+
86
+ @classmethod
87
+ def add_hook(cls, hook: Hook) -> None:
88
+ with cls._lock:
89
+ if hook.key in cls._hooks_by_key:
90
+ raise cls.HookAlreadyPresentError(hook.key)
91
+
92
+ check.isinstance(hook.priority, int)
93
+
94
+ cls._hooks_by_key[hook.key] = hook
95
+ cls._rebuild_hook_collections()
96
+
97
+ cls._install()
98
+
99
+ @classmethod
100
+ def try_add_hook(cls, hook: Hook) -> bool:
101
+ if hook.key in cls._hook_keys:
102
+ return False
103
+
104
+ try:
105
+ cls.add_hook(hook)
106
+ except cls.HookAlreadyPresentError:
107
+ return False
108
+ else:
109
+ return True
110
+
111
+ @classmethod
112
+ def contains_hook(cls, key: ta.Any) -> bool:
113
+ return key in cls._hook_keys
114
+
115
+ @classmethod
116
+ def remove_hook(cls, key: ta.Any) -> None:
117
+ with cls._lock:
118
+ del cls._hooks_by_key[key]
119
+
120
+ cls._rebuild_hook_collections()
121
+
122
+ #
123
+
124
+ @classmethod
125
+ def _before_fork(cls) -> None:
126
+ cls._lock.acquire()
127
+
128
+ for hook in cls._priority_ordered_hooks:
129
+ if (fn := hook.before_fork) is not None:
130
+ fn()
131
+
132
+ @classmethod
133
+ def _after_fork_in_parent(cls) -> None:
134
+ for hook in cls._priority_ordered_hooks:
135
+ if (fn := hook.after_fork_in_parent) is not None:
136
+ fn()
137
+
138
+ cls._lock.release()
139
+
140
+ @classmethod
141
+ def _after_fork_in_child(cls) -> None:
142
+ for hook in cls._priority_ordered_hooks:
143
+ if (fn := hook.after_fork_in_child) is not None:
144
+ fn()
145
+
146
+ cls._lock.release()
147
+
148
+
149
+ #
150
+
151
+
152
+ class ForkHook(abc.ABC): # noqa
153
+ @ta.final
154
+ def __new__(cls, *args, **kwargs): # noqa
155
+ raise TypeError
156
+
157
+ @classmethod
158
+ @ta.final
159
+ def install(cls) -> bool:
160
+ if _ForkHookManager.contains_hook(cls):
161
+ return False
162
+
163
+ return _ForkHookManager.try_add_hook(_ForkHookManager.Hook(
164
+ key=cls,
165
+ priority=cls._hook_priority,
166
+
167
+ before_fork=cls._before_fork,
168
+ after_fork_in_parent=cls._after_fork_in_parent,
169
+ after_fork_in_child=cls._after_fork_in_child,
170
+ ))
171
+
172
+ def __init_subclass__(cls, install: bool = False, **kwargs: ta.Any) -> None:
173
+ super().__init_subclass__(**kwargs)
174
+
175
+ if install:
176
+ cls.install()
177
+
178
+ #
179
+
180
+ _hook_priority: ta.ClassVar[int] = 0
181
+
182
+ @classmethod # noqa
183
+ def _before_fork(cls) -> None:
184
+ pass
185
+
186
+ @classmethod # noqa
187
+ def _after_fork_in_parent(cls) -> None:
188
+ pass
189
+
190
+ @classmethod # noqa
191
+ def _after_fork_in_child(cls) -> None:
192
+ pass
193
+
194
+
195
+ ##
196
+
197
+
198
+ class _ForkDepthTracker(ForkHook):
199
+ _hook_priority = -1000
200
+
201
+ _fork_depth: ta.ClassVar[int] = 0
202
+
203
+ @classmethod
204
+ def _after_fork_in_child(cls) -> None:
205
+ cls._fork_depth += 1
206
+
207
+ @classmethod
208
+ def get_fork_depth(cls) -> int:
209
+ cls.install()
210
+
211
+ return cls._fork_depth
212
+
213
+
214
+ def get_fork_depth() -> int:
215
+ return _ForkDepthTracker.get_fork_depth()
@@ -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