omlish 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +2 -3
- omlish/argparse.py +8 -8
- omlish/asyncs/__init__.py +2 -2
- omlish/asyncs/anyio.py +64 -1
- omlish/asyncs/asyncs.py +1 -3
- omlish/asyncs/futures.py +16 -15
- omlish/c3.py +5 -5
- omlish/check.py +8 -8
- omlish/collections/__init__.py +98 -63
- omlish/collections/_abc.py +2 -0
- omlish/collections/_io_abc.py +4 -2
- omlish/collections/cache/__init__.py +1 -1
- omlish/collections/cache/descriptor.py +12 -12
- omlish/collections/cache/impl.py +27 -20
- omlish/collections/cache/types.py +1 -1
- omlish/collections/coerce.py +44 -44
- omlish/collections/frozen.py +9 -9
- omlish/collections/identity.py +4 -5
- omlish/collections/mappings.py +5 -5
- omlish/collections/ordered.py +8 -8
- omlish/collections/skiplist.py +7 -7
- omlish/collections/sorted.py +4 -4
- omlish/collections/treap.py +42 -17
- omlish/collections/treapmap.py +59 -7
- omlish/collections/unmodifiable.py +25 -24
- omlish/collections/utils.py +1 -1
- omlish/configs/flattening.py +8 -7
- omlish/configs/props.py +3 -3
- omlish/dataclasses/__init__.py +1 -1
- omlish/dataclasses/impl/__init__.py +18 -0
- omlish/dataclasses/impl/api.py +15 -24
- omlish/dataclasses/impl/as_.py +4 -4
- omlish/dataclasses/impl/exceptions.py +1 -1
- omlish/dataclasses/impl/fields.py +8 -8
- omlish/dataclasses/impl/frozen.py +2 -2
- omlish/dataclasses/impl/init.py +6 -6
- omlish/dataclasses/impl/internals.py +16 -1
- omlish/dataclasses/impl/main.py +4 -4
- omlish/dataclasses/impl/metaclass.py +2 -2
- omlish/dataclasses/impl/metadata.py +1 -1
- omlish/dataclasses/impl/order.py +2 -2
- omlish/dataclasses/impl/params.py +4 -38
- omlish/dataclasses/impl/reflect.py +1 -7
- omlish/dataclasses/impl/replace.py +1 -1
- omlish/dataclasses/impl/repr.py +24 -6
- omlish/dataclasses/impl/simple.py +2 -2
- omlish/dataclasses/impl/slots.py +2 -2
- omlish/dataclasses/impl/utils.py +7 -7
- omlish/defs.py +13 -17
- omlish/diag/procfs.py +334 -0
- omlish/diag/ps.py +47 -0
- omlish/{replserver → diag/replserver}/console.py +26 -28
- omlish/{replserver → diag/replserver}/server.py +12 -12
- omlish/dispatch/dispatch.py +14 -16
- omlish/dispatch/functions.py +1 -1
- omlish/dispatch/methods.py +6 -7
- omlish/docker.py +8 -6
- omlish/dynamic.py +13 -13
- omlish/fnpairs.py +311 -0
- omlish/graphs/dot/items.py +1 -1
- omlish/graphs/trees.py +25 -31
- omlish/inject/__init__.py +7 -7
- omlish/inject/elements.py +2 -2
- omlish/inject/exceptions.py +8 -8
- omlish/inject/impl/elements.py +4 -4
- omlish/inject/impl/injector.py +6 -6
- omlish/inject/impl/inspect.py +3 -3
- omlish/inject/impl/scopes.py +9 -9
- omlish/inject/injector.py +1 -1
- omlish/inject/providers.py +2 -2
- omlish/inject/proxy.py +5 -5
- omlish/iterators.py +62 -26
- omlish/json.py +7 -6
- omlish/lang/__init__.py +172 -112
- omlish/lang/cached.py +15 -10
- omlish/lang/classes/__init__.py +35 -24
- omlish/lang/classes/abstract.py +3 -3
- omlish/lang/classes/restrict.py +14 -14
- omlish/lang/classes/simple.py +2 -2
- omlish/lang/classes/virtual.py +5 -5
- omlish/lang/clsdct.py +2 -2
- omlish/lang/cmp.py +2 -2
- omlish/lang/contextmanagers.py +31 -25
- omlish/lang/datetimes.py +1 -1
- omlish/lang/descriptors.py +51 -6
- omlish/lang/exceptions.py +2 -0
- omlish/lang/functions.py +101 -35
- omlish/lang/imports.py +25 -30
- omlish/lang/iterables.py +2 -2
- omlish/lang/maybes.py +2 -1
- omlish/lang/objects.py +17 -11
- omlish/lang/resolving.py +1 -1
- omlish/lang/strings.py +1 -1
- omlish/lang/timeouts.py +53 -0
- omlish/lang/typing.py +5 -5
- omlish/libc.py +15 -11
- omlish/logs/_abc.py +5 -1
- omlish/logs/filters.py +2 -0
- omlish/logs/formatters.py +6 -2
- omlish/logs/utils.py +1 -1
- omlish/marshal/base.py +9 -9
- omlish/marshal/dataclasses.py +2 -2
- omlish/marshal/enums.py +2 -2
- omlish/marshal/exceptions.py +1 -1
- omlish/marshal/factories.py +10 -10
- omlish/marshal/global_.py +10 -4
- omlish/marshal/iterables.py +2 -2
- omlish/marshal/mappings.py +2 -2
- omlish/marshal/objects.py +1 -2
- omlish/marshal/optionals.py +4 -4
- omlish/marshal/polymorphism.py +4 -4
- omlish/marshal/registries.py +3 -3
- omlish/marshal/standard.py +6 -6
- omlish/marshal/utils.py +3 -3
- omlish/marshal/values.py +1 -1
- omlish/math.py +9 -9
- omlish/os.py +13 -4
- omlish/reflect.py +5 -15
- omlish/sql/__init__.py +0 -0
- omlish/sql/_abc.py +65 -0
- omlish/sql/dbs.py +90 -0
- omlish/stats.py +7 -8
- omlish/term.py +1 -1
- omlish/testing/pydevd.py +30 -12
- omlish/testing/pytest/inject/__init__.py +7 -0
- omlish/testing/pytest/inject/harness.py +24 -2
- omlish/testing/pytest/plugins/__init__.py +1 -1
- omlish/testing/pytest/plugins/pydevd.py +12 -0
- omlish/testing/pytest/plugins/switches.py +3 -3
- omlish/testing/testing.py +5 -5
- omlish/text/delimit.py +3 -6
- omlish/text/parts.py +3 -3
- omlish-0.0.0.dev3.dist-info/METADATA +31 -0
- omlish-0.0.0.dev3.dist-info/RECORD +191 -0
- {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/WHEEL +1 -1
- omlish/lang/classes/test/test_abstract.py +0 -89
- omlish/lang/classes/test/test_restrict.py +0 -71
- omlish/lang/classes/test/test_simple.py +0 -58
- omlish/lang/classes/test/test_virtual.py +0 -72
- omlish/testing/pytest/plugins/pycharm.py +0 -54
- omlish-0.0.0.dev1.dist-info/METADATA +0 -17
- omlish-0.0.0.dev1.dist-info/RECORD +0 -187
- /omlish/{lang/classes/test → diag}/__init__.py +0 -0
- /omlish/{replserver → diag/replserver}/__init__.py +0 -0
- /omlish/{replserver → diag/replserver}/__main__.py +0 -0
- {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/top_level.txt +0 -0
omlish/diag/procfs.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- dataclasses
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import resource
|
|
10
|
+
import struct
|
|
11
|
+
import sys
|
|
12
|
+
import typing as ta
|
|
13
|
+
|
|
14
|
+
from .. import iterators as it
|
|
15
|
+
from .. import json
|
|
16
|
+
from .. import lang
|
|
17
|
+
from .. import os as oos
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PidLike = int | str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RLIMIT_RESOURCES = {
|
|
27
|
+
getattr(resource, k): k
|
|
28
|
+
for k in dir(resource)
|
|
29
|
+
if k.startswith('RLIMIT_')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_size(s: str) -> int:
|
|
34
|
+
if ' ' not in s:
|
|
35
|
+
return int(s)
|
|
36
|
+
us = {'kB': 1024, 'mB': 1024 * 1024}
|
|
37
|
+
v, u = s.split()
|
|
38
|
+
return int(v) * us[u]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProcStat(lang.Namespace):
|
|
42
|
+
PID = 0
|
|
43
|
+
COMM = 1
|
|
44
|
+
STATE = 2
|
|
45
|
+
PPID = 3
|
|
46
|
+
PGRP = 4
|
|
47
|
+
SESSION = 5
|
|
48
|
+
TTY_NR = 6
|
|
49
|
+
TPGID = 7
|
|
50
|
+
FLAGS = 8
|
|
51
|
+
MINFLT = 9
|
|
52
|
+
CMINFLT = 10
|
|
53
|
+
MAJFLT = 11
|
|
54
|
+
CMAJFLT = 12
|
|
55
|
+
UTIME = 13
|
|
56
|
+
STIME = 14
|
|
57
|
+
CUTIME = 15
|
|
58
|
+
CSTIME = 16
|
|
59
|
+
PRIORITY = 17
|
|
60
|
+
NICE = 18
|
|
61
|
+
NUM_THREADS = 19
|
|
62
|
+
ITREALVALUE = 20
|
|
63
|
+
STARTTIME = 21
|
|
64
|
+
VSIZE = 22
|
|
65
|
+
RSS = 23
|
|
66
|
+
RSSLIM = 24
|
|
67
|
+
STARTCODE = 25
|
|
68
|
+
ENDCODE = 26
|
|
69
|
+
STARTSTACK = 27
|
|
70
|
+
KSTKESP = 28
|
|
71
|
+
KSTKEIP = 29
|
|
72
|
+
SIGNAL = 30
|
|
73
|
+
BLOCKED = 31
|
|
74
|
+
SIGIGNORE = 32
|
|
75
|
+
SIGCATCH = 33
|
|
76
|
+
WCHAN = 34
|
|
77
|
+
NSWAP = 35
|
|
78
|
+
CNSWAP = 36
|
|
79
|
+
EXIT_SIGNAL = 37
|
|
80
|
+
PROCESSOR = 38
|
|
81
|
+
RT_PRIORITY = 39
|
|
82
|
+
POLICY = 40
|
|
83
|
+
DELAYACCT_BLKIO_TICKS = 41
|
|
84
|
+
GUEST_TIME = 42
|
|
85
|
+
CGUEST_TIME = 43
|
|
86
|
+
START_DATA = 44
|
|
87
|
+
END_DATA = 45
|
|
88
|
+
START_BRK = 46
|
|
89
|
+
ARG_START = 47
|
|
90
|
+
ARG_END = 48
|
|
91
|
+
ENV_START = 49
|
|
92
|
+
ENV_END = 50
|
|
93
|
+
EXIT_CODE = 51
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _check_linux() -> None:
|
|
97
|
+
if sys.platform != 'linux':
|
|
98
|
+
raise OSError
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_process_stats(pid: PidLike = 'self') -> list[str]:
|
|
102
|
+
"""http://man7.org/linux/man-pages/man5/proc.5.html -> /proc/[pid]/stat"""
|
|
103
|
+
|
|
104
|
+
_check_linux()
|
|
105
|
+
with open(f'/proc/{pid}/stat') as f:
|
|
106
|
+
buf = f.read()
|
|
107
|
+
l, _, r = buf.rpartition(')')
|
|
108
|
+
pid, _, comm = l.partition('(')
|
|
109
|
+
return [pid.strip(), comm, *r.strip().split(' ')]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_process_chain(pid: PidLike = 'self') -> list[tuple[int, str]]:
|
|
113
|
+
_check_linux()
|
|
114
|
+
lst = []
|
|
115
|
+
while pid:
|
|
116
|
+
process_stats = get_process_stats(pid)
|
|
117
|
+
lst.append((int(process_stats[ProcStat.PID]), process_stats[ProcStat.COMM]))
|
|
118
|
+
pid = int(process_stats[ProcStat.PPID])
|
|
119
|
+
return lst
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_process_start_time(pid: PidLike = 'self') -> int:
|
|
123
|
+
"""https://stackoverflow.com/questions/2598145/how-to-retrieve-the-process-start-time-or-uptime-in-python"""
|
|
124
|
+
|
|
125
|
+
_check_linux()
|
|
126
|
+
hz = os.sysconf(os.sysconf_names['SC_CLK_TCK'])
|
|
127
|
+
with open('/proc/stat') as f:
|
|
128
|
+
system_stats = f.readlines()
|
|
129
|
+
for line in system_stats:
|
|
130
|
+
if line.startswith('btime'):
|
|
131
|
+
boot_timestamp = int(line.split()[1])
|
|
132
|
+
break
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError
|
|
135
|
+
process_stats = get_process_stats(pid)
|
|
136
|
+
age_from_boot_jiffies = int(process_stats[ProcStat.STARTTIME])
|
|
137
|
+
age_from_boot_timestamp = age_from_boot_jiffies // hz
|
|
138
|
+
return boot_timestamp + age_from_boot_timestamp
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_process_rss(pid: PidLike = 'self') -> int:
|
|
142
|
+
return int(get_process_stats(pid)[ProcStat.RSS])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def set_process_oom_score_adj(score: str, pid: PidLike = 'self') -> None:
|
|
146
|
+
_check_linux()
|
|
147
|
+
with open(f'/proc/{pid}/oom_score_adj', 'w') as f:
|
|
148
|
+
f.write(str(score))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
MAP_LINE_RX = re.compile(
|
|
152
|
+
r'^'
|
|
153
|
+
r'(?P<address>[A-Fa-f0-9]+)-(?P<end_address>[A-Fa-f0-9]+)\s+'
|
|
154
|
+
r'(?P<permissions>\S+)\s+'
|
|
155
|
+
r'(?P<offset>[A-Fa-f0-9]+)\s+'
|
|
156
|
+
r'(?P<device>\S+)\s+'
|
|
157
|
+
r'(?P<inode>\d+)\s+'
|
|
158
|
+
r'(?P<path>.*)'
|
|
159
|
+
r'$'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_process_maps(pid: PidLike = 'self', sharing: bool = False) -> ta.Iterator[dict[str, ta.Any]]:
|
|
164
|
+
"""http://man7.org/linux/man-pages/man5/proc.5.html -> /proc/[pid]/maps"""
|
|
165
|
+
|
|
166
|
+
_check_linux()
|
|
167
|
+
with open(f'/proc/{pid}/{"smaps" if sharing else "maps"}') as map_file:
|
|
168
|
+
while True:
|
|
169
|
+
line = map_file.readline()
|
|
170
|
+
if not line:
|
|
171
|
+
break
|
|
172
|
+
m = MAP_LINE_RX.match(line)
|
|
173
|
+
if not m:
|
|
174
|
+
raise ValueError(line)
|
|
175
|
+
address = int(m.group('address'), 16)
|
|
176
|
+
end_address = int(m.group('end_address'), 16)
|
|
177
|
+
d = {
|
|
178
|
+
'address': address,
|
|
179
|
+
'end_address': end_address,
|
|
180
|
+
'size': end_address - address,
|
|
181
|
+
'permissions': [x for x in m.group('permissions') if x != '-'],
|
|
182
|
+
'offset': int(m.group('offset'), 16),
|
|
183
|
+
'device': m.group('device'),
|
|
184
|
+
'inode': int(m.group('inode')),
|
|
185
|
+
'path': m.group('path'),
|
|
186
|
+
}
|
|
187
|
+
if sharing:
|
|
188
|
+
s: dict[str, ta.Any] = {}
|
|
189
|
+
while True:
|
|
190
|
+
line = map_file.readline()
|
|
191
|
+
k, v = line.split(':')
|
|
192
|
+
if k.lower() == 'vmflags':
|
|
193
|
+
break
|
|
194
|
+
s[k.lower()] = parse_size(v.strip())
|
|
195
|
+
_, v = line.split(':')
|
|
196
|
+
s['vmflags'] = [p for p in [j.strip() for j in v.split(' ')] if p]
|
|
197
|
+
d['sharing'] = s
|
|
198
|
+
yield d
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
PAGEMAP_KEYS = (
|
|
202
|
+
'address',
|
|
203
|
+
'pfn',
|
|
204
|
+
'swap_type',
|
|
205
|
+
'swap_offset',
|
|
206
|
+
'pte_soft_dirty',
|
|
207
|
+
'file_page_or_shared_anon',
|
|
208
|
+
'page_swapped',
|
|
209
|
+
'page_present',
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_process_range_pagemaps(start: int, end: int, pid: PidLike = 'self') -> ta.Iterable[dict[str, int]]:
|
|
214
|
+
"""https://www.kernel.org/doc/Documentation/vm/pagemap.txt"""
|
|
215
|
+
|
|
216
|
+
_check_linux()
|
|
217
|
+
offset = (start // oos.PAGE_SIZE) * 8
|
|
218
|
+
npages = ((end - start) // oos.PAGE_SIZE)
|
|
219
|
+
size = npages * 8
|
|
220
|
+
with open(f'/proc/{pid}/pagemap', 'rb') as pagemap_file:
|
|
221
|
+
pagemap_file.seek(offset)
|
|
222
|
+
pagemap_buf = pagemap_file.read(size)
|
|
223
|
+
if not pagemap_buf:
|
|
224
|
+
return
|
|
225
|
+
_struct_unpack = struct.unpack
|
|
226
|
+
for pagenum in range(npages):
|
|
227
|
+
[packed] = _struct_unpack('Q', pagemap_buf[pagenum * 8:(pagenum + 1) * 8])
|
|
228
|
+
yield {
|
|
229
|
+
'address': start + (pagenum * oos.PAGE_SIZE),
|
|
230
|
+
'pfn': (packed & ((1 << (54 + 1)) - 1)),
|
|
231
|
+
'swap_type': (packed & ((1 << (4 + 1)) - 1)),
|
|
232
|
+
'swap_offset': (packed & ((1 << (54 + 1)) - 1)) >> 5,
|
|
233
|
+
'pte_soft_dirty': ((packed >> 55) & 1) > 0,
|
|
234
|
+
'file_page_or_shared_anon': ((packed >> 61) & 1) > 0,
|
|
235
|
+
'page_swapped': ((packed >> 62) & 1) > 0,
|
|
236
|
+
'page_present': ((packed >> 63) & 1) > 0,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_process_pagemaps(pid: PidLike = 'self') -> ta.Iterable[dict[str, int]]:
|
|
241
|
+
_check_linux()
|
|
242
|
+
for m in get_process_maps(pid):
|
|
243
|
+
yield from get_process_range_pagemaps(m['address'], m['end_address'], pid)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _dump_cmd(args: ta.Any) -> None:
|
|
247
|
+
total = 0
|
|
248
|
+
dirty_total = 0
|
|
249
|
+
for m in get_process_maps(args.pid, sharing=True):
|
|
250
|
+
total += m['sharing']['rss']
|
|
251
|
+
sys.stdout.write(json.dumps({'map': m}))
|
|
252
|
+
sys.stdout.write('\n')
|
|
253
|
+
for pm in get_process_range_pagemaps(m['address'], m['end_address'], args.pid):
|
|
254
|
+
if pm['pte_soft_dirty']:
|
|
255
|
+
dirty_total += oos.PAGE_SIZE
|
|
256
|
+
sys.stdout.write(json.dumps({'page': tuple(pm[k] for k in PAGEMAP_KEYS)}))
|
|
257
|
+
sys.stdout.write('\n')
|
|
258
|
+
dct = {
|
|
259
|
+
'total': total,
|
|
260
|
+
'dirty_total': dirty_total,
|
|
261
|
+
}
|
|
262
|
+
sys.stdout.write(json.dumps(dct))
|
|
263
|
+
sys.stdout.write('\n')
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _cmp_cmd(args: ta.Any) -> None:
|
|
267
|
+
if len(args.pids) == 1:
|
|
268
|
+
[rpid] = args.pids
|
|
269
|
+
lpid = get_process_chain(rpid)[1][0]
|
|
270
|
+
elif len(args.pids) == 2:
|
|
271
|
+
lpid, rpid = args.pids
|
|
272
|
+
else:
|
|
273
|
+
raise TypeError('Invalid arguments')
|
|
274
|
+
|
|
275
|
+
def g(pid: int) -> ta.Iterator[dict[str, int]]:
|
|
276
|
+
for m in get_process_maps(pid, sharing=True):
|
|
277
|
+
yield from get_process_range_pagemaps(m['address'], m['end_address'], pid)
|
|
278
|
+
|
|
279
|
+
lpms, rpms = (g(pid) for pid in (lpid, rpid))
|
|
280
|
+
|
|
281
|
+
l_pages = 0
|
|
282
|
+
r_pages = 0
|
|
283
|
+
c_pages = 0
|
|
284
|
+
for _, ps in it.merge_on(lambda pm: pm['address'], lpms, rpms):
|
|
285
|
+
l, r = it.expand_indexed_pairs(ps, None, width=2)
|
|
286
|
+
if l is not None and r is None:
|
|
287
|
+
l_pages += 1
|
|
288
|
+
elif l is None and r is not None:
|
|
289
|
+
r_pages += 1
|
|
290
|
+
elif l['pfn'] != r['pfn']: # type: ignore
|
|
291
|
+
c_pages += 1
|
|
292
|
+
else:
|
|
293
|
+
continue
|
|
294
|
+
if not args.quiet:
|
|
295
|
+
sys.stdout.write(json.dumps([l, r]))
|
|
296
|
+
sys.stdout.write('\n')
|
|
297
|
+
l_pages += c_pages
|
|
298
|
+
r_pages += c_pages
|
|
299
|
+
dct = {
|
|
300
|
+
'l_pages': l_pages,
|
|
301
|
+
'l_bytes': l_pages * oos.PAGE_SIZE,
|
|
302
|
+
'r_pages': r_pages,
|
|
303
|
+
'r_bytes': r_pages * oos.PAGE_SIZE,
|
|
304
|
+
'c_pages': c_pages,
|
|
305
|
+
'c_bytes': c_pages * oos.PAGE_SIZE,
|
|
306
|
+
}
|
|
307
|
+
sys.stdout.write(json.dumps(dct))
|
|
308
|
+
sys.stdout.write('\n')
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _main() -> None:
|
|
312
|
+
_check_linux()
|
|
313
|
+
|
|
314
|
+
arg_parser = argparse.ArgumentParser()
|
|
315
|
+
arg_parser.add_argument('-q', '--quiet', action='store_true')
|
|
316
|
+
arg_subparsers = arg_parser.add_subparsers()
|
|
317
|
+
|
|
318
|
+
dump_arg_parser = arg_subparsers.add_parser('dump')
|
|
319
|
+
dump_arg_parser.add_argument('pid', type=int)
|
|
320
|
+
dump_arg_parser.set_defaults(func=_dump_cmd)
|
|
321
|
+
|
|
322
|
+
cmp_arg_parser = arg_subparsers.add_parser('cmp')
|
|
323
|
+
cmp_arg_parser.add_argument('pids', type=int, nargs='*')
|
|
324
|
+
cmp_arg_parser.set_defaults(func=_cmp_cmd)
|
|
325
|
+
|
|
326
|
+
args = arg_parser.parse_args()
|
|
327
|
+
if not hasattr(args, 'func'):
|
|
328
|
+
arg_parser.print_help()
|
|
329
|
+
else:
|
|
330
|
+
args.func(args)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == '__main__':
|
|
334
|
+
_main()
|
omlish/diag/ps.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from .. import lang
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dc.dataclass(frozen=True)
|
|
9
|
+
class PsItem:
|
|
10
|
+
pid: int
|
|
11
|
+
ppid: int
|
|
12
|
+
cmd: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_ps_item(pid: int, timeout: lang.Timeout | None = None) -> PsItem:
|
|
16
|
+
timeout = lang.timeout(timeout)
|
|
17
|
+
out = subprocess.check_output(
|
|
18
|
+
['ps', '-o', 'pid=,ppid=,command=', str(int(pid))],
|
|
19
|
+
timeout=timeout.or_(None),
|
|
20
|
+
).decode().strip()
|
|
21
|
+
opid, _, rest = out.partition(' ')
|
|
22
|
+
ppid, _, cmd = rest.strip().partition(' ')
|
|
23
|
+
return PsItem(
|
|
24
|
+
int(opid),
|
|
25
|
+
int(ppid),
|
|
26
|
+
cmd.strip(),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_ps_lineage(pid: int, timeout: lang.Timeout | None = None) -> list[PsItem]:
|
|
31
|
+
timeout = lang.timeout(timeout)
|
|
32
|
+
ret: list[PsItem] = []
|
|
33
|
+
while True:
|
|
34
|
+
cur = get_ps_item(pid, timeout)
|
|
35
|
+
if cur.ppid < 1:
|
|
36
|
+
break
|
|
37
|
+
ret.append(cur)
|
|
38
|
+
pid = cur.ppid
|
|
39
|
+
return ret
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _main() -> None:
|
|
43
|
+
print(get_ps_lineage(os.getpid()))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
_main()
|
|
@@ -25,13 +25,13 @@ import traceback
|
|
|
25
25
|
import types
|
|
26
26
|
import typing as ta
|
|
27
27
|
|
|
28
|
-
from
|
|
28
|
+
from ... import check
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
log = logging.getLogger(__name__)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class
|
|
34
|
+
class DisconnectError(Exception):
|
|
35
35
|
pass
|
|
36
36
|
|
|
37
37
|
|
|
@@ -43,13 +43,13 @@ class InteractiveSocketConsole:
|
|
|
43
43
|
def __init__(
|
|
44
44
|
self,
|
|
45
45
|
conn: sock.socket,
|
|
46
|
-
locals:
|
|
47
|
-
filename: str = '<console>'
|
|
46
|
+
locals: dict[str, ta.Any] | None = None, # noqa
|
|
47
|
+
filename: str = '<console>',
|
|
48
48
|
) -> None:
|
|
49
49
|
super().__init__()
|
|
50
50
|
|
|
51
51
|
if locals is None:
|
|
52
|
-
locals = {
|
|
52
|
+
locals = { # noqa
|
|
53
53
|
'__name__': '__console__',
|
|
54
54
|
'__doc__': None,
|
|
55
55
|
'__console__': self,
|
|
@@ -73,7 +73,7 @@ class InteractiveSocketConsole:
|
|
|
73
73
|
|
|
74
74
|
CPRT = 'Type "help", "copyright", "credits" or "license" for more information.'
|
|
75
75
|
|
|
76
|
-
def interact(self, banner:
|
|
76
|
+
def interact(self, banner: str | None = None, exitmsg: str | None = None) -> None:
|
|
77
77
|
log.info(f'Console {id(self)} on thread {threading.current_thread().ident} interacting')
|
|
78
78
|
|
|
79
79
|
try:
|
|
@@ -81,11 +81,9 @@ class InteractiveSocketConsole:
|
|
|
81
81
|
ps2 = getattr(sys, 'ps2', '... ')
|
|
82
82
|
|
|
83
83
|
if banner is None:
|
|
84
|
-
self.write(
|
|
85
|
-
'Python %s on %s\n%s\n(%s)\n' %
|
|
86
|
-
(sys.version, sys.platform, self.CPRT, self.__class__.__name__))
|
|
84
|
+
self.write(f'Python {sys.version} on {sys.platform}\n{self.CPRT}\n({self.__class__.__name__})\n')
|
|
87
85
|
elif banner:
|
|
88
|
-
self.write('
|
|
86
|
+
self.write(f'{banner!s}\n')
|
|
89
87
|
|
|
90
88
|
more = False
|
|
91
89
|
while True:
|
|
@@ -104,12 +102,12 @@ class InteractiveSocketConsole:
|
|
|
104
102
|
more = False
|
|
105
103
|
|
|
106
104
|
if exitmsg is None:
|
|
107
|
-
self.write('now exiting
|
|
105
|
+
self.write(f'now exiting {self.__class__.__name__}...\n')
|
|
108
106
|
|
|
109
107
|
elif exitmsg != '':
|
|
110
|
-
self.write('
|
|
108
|
+
self.write(f'{exitmsg}\n')
|
|
111
109
|
|
|
112
|
-
except
|
|
110
|
+
except DisconnectError:
|
|
113
111
|
pass
|
|
114
112
|
|
|
115
113
|
except OSError as oe:
|
|
@@ -133,7 +131,7 @@ class InteractiveSocketConsole:
|
|
|
133
131
|
while True:
|
|
134
132
|
b = self._conn.recv(1)
|
|
135
133
|
if not b:
|
|
136
|
-
raise
|
|
134
|
+
raise DisconnectError
|
|
137
135
|
if b == b'\n':
|
|
138
136
|
break
|
|
139
137
|
buf += b
|
|
@@ -144,10 +142,10 @@ class InteractiveSocketConsole:
|
|
|
144
142
|
|
|
145
143
|
def compile(
|
|
146
144
|
self,
|
|
147
|
-
source:
|
|
145
|
+
source: str | ast.AST,
|
|
148
146
|
filename: str = '<input>',
|
|
149
|
-
symbol: str = 'single'
|
|
150
|
-
) ->
|
|
147
|
+
symbol: str = 'single',
|
|
148
|
+
) -> types.CodeType | None:
|
|
151
149
|
if isinstance(source, ast.AST):
|
|
152
150
|
return self._compiler.compiler(source, filename, symbol) # type: ignore
|
|
153
151
|
else:
|
|
@@ -155,7 +153,7 @@ class InteractiveSocketConsole:
|
|
|
155
153
|
|
|
156
154
|
def run_source(
|
|
157
155
|
self,
|
|
158
|
-
source:
|
|
156
|
+
source: str | ast.AST,
|
|
159
157
|
filename: str = '<input>',
|
|
160
158
|
symbol: str = 'single',
|
|
161
159
|
) -> bool:
|
|
@@ -195,7 +193,7 @@ class InteractiveSocketConsole:
|
|
|
195
193
|
expr.value,
|
|
196
194
|
lineno=expr.lineno,
|
|
197
195
|
col_offset=expr.col_offset,
|
|
198
|
-
)
|
|
196
|
+
),
|
|
199
197
|
],
|
|
200
198
|
)
|
|
201
199
|
ast.fix_missing_locations(source)
|
|
@@ -227,21 +225,21 @@ class InteractiveSocketConsole:
|
|
|
227
225
|
finally:
|
|
228
226
|
last_tb = ei = None # type: ignore # noqa
|
|
229
227
|
|
|
230
|
-
def show_syntax_error(self, filename:
|
|
231
|
-
|
|
232
|
-
sys.last_type =
|
|
233
|
-
sys.last_value =
|
|
228
|
+
def show_syntax_error(self, filename: str | None = None) -> None:
|
|
229
|
+
et, e, tb = sys.exc_info()
|
|
230
|
+
sys.last_type = et
|
|
231
|
+
sys.last_value = e
|
|
234
232
|
sys.last_traceback = tb
|
|
235
|
-
if filename and
|
|
233
|
+
if filename and et is SyntaxError:
|
|
236
234
|
# Work hard to stuff the correct filename in the exception
|
|
237
235
|
try:
|
|
238
|
-
msg, (dummy_filename, lineno, offset, line) =
|
|
236
|
+
msg, (dummy_filename, lineno, offset, line) = e.args # type: ignore
|
|
239
237
|
except ValueError:
|
|
240
238
|
# Not the format we expect; leave it alone
|
|
241
239
|
pass
|
|
242
240
|
else:
|
|
243
241
|
# Stuff in the right filename
|
|
244
|
-
|
|
245
|
-
sys.last_value =
|
|
246
|
-
lines = traceback.format_exception_only(
|
|
242
|
+
e = SyntaxError(msg, (filename, lineno, offset, line))
|
|
243
|
+
sys.last_value = e
|
|
244
|
+
lines = traceback.format_exception_only(et, e)
|
|
247
245
|
self.write(''.join(lines))
|
|
@@ -23,8 +23,8 @@ import threading
|
|
|
23
23
|
import typing as ta
|
|
24
24
|
import weakref
|
|
25
25
|
|
|
26
|
-
from
|
|
27
|
-
from
|
|
26
|
+
from ... import check
|
|
27
|
+
from ... import dataclasses as dc
|
|
28
28
|
from .console import InteractiveSocketConsole
|
|
29
29
|
|
|
30
30
|
|
|
@@ -38,7 +38,7 @@ class ReplServer:
|
|
|
38
38
|
@dc.dataclass(frozen=True)
|
|
39
39
|
class Config:
|
|
40
40
|
path: str
|
|
41
|
-
file_mode:
|
|
41
|
+
file_mode: int | None = None
|
|
42
42
|
poll_interval: float = 0.5
|
|
43
43
|
exit_timeout: float = 10.0
|
|
44
44
|
|
|
@@ -51,7 +51,7 @@ class ReplServer:
|
|
|
51
51
|
check.not_empty(config.path)
|
|
52
52
|
self._config = check.isinstance(config, ReplServer.Config)
|
|
53
53
|
|
|
54
|
-
self._socket:
|
|
54
|
+
self._socket: sock.socket | None = None
|
|
55
55
|
self._is_running = False
|
|
56
56
|
self._consoles_by_threads: ta.MutableMapping[threading.Thread, InteractiveSocketConsole] = \
|
|
57
57
|
weakref.WeakKeyDictionary() # noqa
|
|
@@ -62,12 +62,12 @@ class ReplServer:
|
|
|
62
62
|
def path(self) -> str:
|
|
63
63
|
return self._config.path
|
|
64
64
|
|
|
65
|
-
def __enter__(self):
|
|
65
|
+
def __enter__(self) -> ta.Self:
|
|
66
66
|
check.state(not self._is_running)
|
|
67
67
|
check.state(not self._is_shutdown.is_set())
|
|
68
68
|
return self
|
|
69
69
|
|
|
70
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
70
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
71
71
|
if not self._is_shutdown.is_set():
|
|
72
72
|
self.shutdown(True, self._config.exit_timeout)
|
|
73
73
|
|
|
@@ -91,7 +91,7 @@ class ReplServer:
|
|
|
91
91
|
while not self._should_shutdown:
|
|
92
92
|
try:
|
|
93
93
|
conn, _ = self._socket.accept()
|
|
94
|
-
except
|
|
94
|
+
except TimeoutError:
|
|
95
95
|
continue
|
|
96
96
|
|
|
97
97
|
log.info(f'Got repl server connection on file {self._config.path}')
|
|
@@ -106,7 +106,7 @@ class ReplServer:
|
|
|
106
106
|
log.info(
|
|
107
107
|
f'Starting console {id(console)} repl server connection '
|
|
108
108
|
f'on file {self._config.path} '
|
|
109
|
-
f'on thread {threading.current_thread().ident}'
|
|
109
|
+
f'on thread {threading.current_thread().ident}',
|
|
110
110
|
)
|
|
111
111
|
self._consoles_by_threads[threading.current_thread()] = console
|
|
112
112
|
console.interact()
|
|
@@ -117,13 +117,13 @@ class ReplServer:
|
|
|
117
117
|
name=self.CONNECTION_THREAD_NAME)
|
|
118
118
|
thread.start()
|
|
119
119
|
|
|
120
|
-
for
|
|
120
|
+
for console in self._consoles_by_threads.values():
|
|
121
121
|
try:
|
|
122
122
|
console.conn.close()
|
|
123
123
|
except Exception:
|
|
124
124
|
log.exception('Error shutting down')
|
|
125
125
|
|
|
126
|
-
for thread in self._consoles_by_threads
|
|
126
|
+
for thread in self._consoles_by_threads:
|
|
127
127
|
try:
|
|
128
128
|
thread.join(self._config.exit_timeout)
|
|
129
129
|
except Exception:
|
|
@@ -135,12 +135,12 @@ class ReplServer:
|
|
|
135
135
|
self._is_shutdown.set()
|
|
136
136
|
self._is_running = False
|
|
137
137
|
|
|
138
|
-
def shutdown(self, block: bool = False, timeout:
|
|
138
|
+
def shutdown(self, block: bool = False, timeout: float | None = None) -> None:
|
|
139
139
|
self._should_shutdown = True
|
|
140
140
|
if block:
|
|
141
141
|
self._is_shutdown.wait(timeout=timeout)
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def run():
|
|
144
|
+
def run() -> None:
|
|
145
145
|
with ReplServer(ReplServer.Config('repl.sock')) as repl_server:
|
|
146
146
|
repl_server.run()
|
omlish/dispatch/dispatch.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import contextlib
|
|
2
3
|
import typing as ta
|
|
3
4
|
import weakref
|
|
4
5
|
|
|
@@ -13,33 +14,32 @@ T = ta.TypeVar('T')
|
|
|
13
14
|
##
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
_IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable,
|
|
17
|
+
_IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable, frozenset[type]] = weakref.WeakKeyDictionary()
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def get_impl_func_cls_set(func: ta.Callable) ->
|
|
20
|
-
|
|
20
|
+
def get_impl_func_cls_set(func: ta.Callable) -> frozenset[type]:
|
|
21
|
+
with contextlib.suppress(KeyError):
|
|
21
22
|
return _IMPL_FUNC_CLS_SET_CACHE[func]
|
|
22
|
-
except KeyError:
|
|
23
|
-
pass
|
|
24
23
|
|
|
25
24
|
ann = getattr(func, '__annotations__', {})
|
|
26
25
|
if not ann:
|
|
27
26
|
raise TypeError(f'Invalid impl func: {func!r}')
|
|
28
27
|
|
|
29
28
|
_, cls = next(iter(ta.get_type_hints(func).items()))
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
rty = rfl.type_(cls)
|
|
30
|
+
if isinstance(rty, rfl.Union):
|
|
31
|
+
ret = frozenset(check.isinstance(arg, type) for arg in rty.args)
|
|
32
32
|
else:
|
|
33
|
-
ret = frozenset([check.isinstance(
|
|
33
|
+
ret = frozenset([check.isinstance(rty, type)])
|
|
34
34
|
|
|
35
35
|
_IMPL_FUNC_CLS_SET_CACHE[func] = ret
|
|
36
36
|
return ret
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def find_impl(cls: type, registry: ta.Mapping[type, T]) ->
|
|
39
|
+
def find_impl(cls: type, registry: ta.Mapping[type, T]) -> T | None:
|
|
40
40
|
mro = c3.compose_mro(cls, registry.keys())
|
|
41
41
|
|
|
42
|
-
match:
|
|
42
|
+
match: type | None = None
|
|
43
43
|
for t in mro:
|
|
44
44
|
if match is not None:
|
|
45
45
|
# If *match* is an implicit ABC but there is another unrelated, equally matching implicit ABC, refuse the
|
|
@@ -71,23 +71,21 @@ class Dispatcher(ta.Generic[T]):
|
|
|
71
71
|
impls_by_arg_cls: dict[type, T] = {}
|
|
72
72
|
self._impls_by_arg_cls = impls_by_arg_cls
|
|
73
73
|
|
|
74
|
-
dispatch_cache: dict[ta.Any,
|
|
74
|
+
dispatch_cache: dict[ta.Any, T | None] = {}
|
|
75
75
|
self._get_dispatch_cache = lambda: dispatch_cache
|
|
76
76
|
|
|
77
77
|
def cache_remove(k, self_ref=weakref.ref(self)):
|
|
78
78
|
if (ref_self := self_ref()) is not None:
|
|
79
79
|
cache = ref_self._get_dispatch_cache() # noqa
|
|
80
|
-
|
|
80
|
+
with contextlib.suppress(KeyError):
|
|
81
81
|
del cache[k]
|
|
82
|
-
except KeyError:
|
|
83
|
-
pass
|
|
84
82
|
|
|
85
83
|
cache_token: ta.Any = None
|
|
86
84
|
self._get_cache_token = lambda: cache_token
|
|
87
85
|
|
|
88
86
|
weakref_ref_ = weakref.ref
|
|
89
87
|
|
|
90
|
-
def dispatch(cls: type) ->
|
|
88
|
+
def dispatch(cls: type) -> T | None:
|
|
91
89
|
nonlocal cache_token
|
|
92
90
|
|
|
93
91
|
if cache_token is not None and (current_token := abc.get_cache_token()) != cache_token:
|
|
@@ -132,6 +130,6 @@ class Dispatcher(ta.Generic[T]):
|
|
|
132
130
|
def cache_size(self) -> int:
|
|
133
131
|
return len(self._get_dispatch_cache())
|
|
134
132
|
|
|
135
|
-
dispatch: ta.Callable[[type],
|
|
133
|
+
dispatch: ta.Callable[[type], T | None]
|
|
136
134
|
|
|
137
135
|
register: ta.Callable[[T, ta.Iterable[type]], T]
|