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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev225'
2
- __revision__ = '99eb1740a6c8647c20c80278b6d0bedff0f7c103'
1
+ __version__ = '0.0.0.dev227'
2
+ __revision__ = '2faa3e7f9120edd3e6dd79fad6401fd87d89f5cf'
3
3
 
4
4
 
5
5
  #
@@ -10,6 +10,7 @@ import sys
10
10
  import typing as ta
11
11
 
12
12
  from ...lite.check import check
13
+ from ...lite.timeouts import TimeoutLike
13
14
  from ...subprocesses.async_ import AbstractAsyncSubprocesses
14
15
  from ...subprocesses.run import SubprocessRun
15
16
  from ...subprocesses.run import SubprocessRunOutput
@@ -130,7 +131,7 @@ class AsyncioProcessCommunicator:
130
131
  async def communicate(
131
132
  self,
132
133
  input: ta.Any = None, # noqa
133
- timeout: ta.Optional[float] = None,
134
+ timeout: ta.Optional[TimeoutLike] = None,
134
135
  ) -> Communication:
135
136
  return await asyncio_maybe_timeout(self._communicate(input), timeout)
136
137
 
@@ -143,7 +144,7 @@ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
143
144
  self,
144
145
  proc: asyncio.subprocess.Process,
145
146
  input: ta.Any = None, # noqa
146
- timeout: ta.Optional[float] = None,
147
+ timeout: ta.Optional[TimeoutLike] = None,
147
148
  ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
148
149
  return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
149
150
 
@@ -154,7 +155,7 @@ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
154
155
  self,
155
156
  *cmd: str,
156
157
  shell: bool = False,
157
- timeout: ta.Optional[float] = None,
158
+ timeout: ta.Optional[TimeoutLike] = None,
158
159
  **kwargs: ta.Any,
159
160
  ) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
160
161
  with self.prepare_and_wrap( *cmd, shell=shell, **kwargs) as (cmd, kwargs): # noqa
@@ -3,14 +3,17 @@
3
3
  import asyncio
4
4
  import typing as ta
5
5
 
6
+ from ...lite.timeouts import Timeout
7
+ from ...lite.timeouts import TimeoutLike
8
+
6
9
 
7
10
  AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
8
11
 
9
12
 
10
13
  def asyncio_maybe_timeout(
11
14
  fut: AwaitableT,
12
- timeout: ta.Optional[float] = None,
15
+ timeout: ta.Optional[TimeoutLike] = None,
13
16
  ) -> AwaitableT:
14
17
  if timeout is not None:
15
- fut = asyncio.wait_for(fut, timeout) # type: ignore
18
+ fut = asyncio.wait_for(fut, Timeout.of(timeout)()) # type: ignore
16
19
  return fut
omlish/diag/lslocks.py ADDED
@@ -0,0 +1,64 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ https://github.com/util-linux/util-linux/blob/a4436c7bf07f98a6381c7dfa2ab3f9a415f9c479/misc-utils/lslocks.c
5
+ """
6
+ import dataclasses as dc
7
+ import json
8
+ import typing as ta
9
+
10
+ from ..lite.check import check
11
+ from ..lite.marshal import OBJ_MARSHALER_FIELD_KEY
12
+ from ..lite.marshal import unmarshal_obj
13
+ from ..subprocesses.run import SubprocessRun
14
+ from ..subprocesses.run import SubprocessRunnable
15
+ from ..subprocesses.run import SubprocessRunOutput
16
+
17
+
18
+ ##
19
+
20
+
21
+ @dc.dataclass(frozen=True)
22
+ class LslocksItem:
23
+ """https://manpages.ubuntu.com/manpages/lunar/man8/lslocks.8.html"""
24
+
25
+ command: str
26
+ pid: int
27
+ type: str # POSIX | FLOCK | OFDLCK
28
+ size: ta.Optional[int]
29
+ mode: str # READ | WRITE
30
+ mandatory: bool = dc.field(metadata={OBJ_MARSHALER_FIELD_KEY: 'm'})
31
+ start: int
32
+ end: int
33
+ path: str
34
+ blocker: ta.Optional[int] = None
35
+
36
+
37
+ ##
38
+
39
+
40
+ @dc.dataclass(frozen=True)
41
+ class LslocksCommand(SubprocessRunnable[ta.List[LslocksItem]]):
42
+ pid: ta.Optional[int] = None
43
+ no_inaccessible: bool = False
44
+
45
+ def make_run(self) -> SubprocessRun:
46
+ return SubprocessRun.of(
47
+ 'lslocks',
48
+ '--json',
49
+ '--bytes',
50
+ '--notruncate',
51
+ *(['--pid', str(self.pid)] if self.pid is not None else []),
52
+ *(['--noinaccessible'] if self.no_inaccessible else []),
53
+
54
+ check=True,
55
+ stdout='pipe',
56
+ stderr='devnull',
57
+ )
58
+
59
+ def handle_run_output(self, output: SubprocessRunOutput) -> ta.List[LslocksItem]:
60
+ buf = check.not_none(output.stdout).decode().strip()
61
+ if not buf:
62
+ return []
63
+ obj = json.loads(buf)
64
+ return unmarshal_obj(obj['locks'], ta.List[LslocksItem])
omlish/diag/lsof.py ADDED
@@ -0,0 +1,264 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ """
4
+ https://man7.org/linux/man-pages/man8/lsof.8.html
5
+
6
+ Included in the process set are fields that identify the command, the process group IDentification (PGID) number, the
7
+ task (thread) ID (TID), and the user ID (UID) number or login name.
8
+ """
9
+ import dataclasses as dc
10
+ import enum
11
+ import typing as ta
12
+
13
+ from ..lite.check import check
14
+ from ..lite.dataclasses import dataclass_repr_omit_falsey
15
+ from ..lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
16
+ from ..subprocesses.run import SubprocessRun
17
+ from ..subprocesses.run import SubprocessRunnable
18
+ from ..subprocesses.run import SubprocessRunOutput
19
+
20
+
21
+ ##
22
+
23
+
24
+ # FIXME: ??
25
+ # https://unix.stackexchange.com/a/86011
26
+ class LsofItemMode(enum.Enum):
27
+ SOLARIS_NFS_LOCK = 'N'
28
+ READ_LOCK_PARTIAL = 'r'
29
+ READ_LOCK_FULL = 'R'
30
+ WRITE_LOCK_PARTIAL = 'w'
31
+ WRITE_LOCK_FULL = 'W'
32
+ READ_WRITE_LOCK = 'u'
33
+ UNKNOWN_LOCK_TYPE = 'U'
34
+ XENIX_LOCK_PARTIAL = 'x'
35
+ XENIX_LOCK_FULL = 'X'
36
+
37
+
38
+ @dc.dataclass(frozen=True)
39
+ class _LsofFieldMeta:
40
+ prefix: str
41
+ process: bool = False
42
+ variadic: bool = False
43
+
44
+ @classmethod
45
+ def make(cls, *args, **kwargs):
46
+ return {
47
+ cls: cls(*args, **kwargs),
48
+ OBJ_MARSHALER_OMIT_IF_NONE: True,
49
+ }
50
+
51
+
52
+ @dc.dataclass(frozen=True)
53
+ class LsofItem:
54
+ __repr__ = dataclass_repr_omit_falsey
55
+
56
+ class _PREFIX:
57
+ def __new__(cls, *args, **kwargs): # noqa
58
+ raise TypeError
59
+
60
+ class _PROCESS:
61
+ def __new__(cls, *args, **kwargs): # noqa
62
+ raise TypeError
63
+
64
+ pid: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('p', process=True))
65
+ pgid: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('g', process=True))
66
+ ppid: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('R', process=True))
67
+ cmd: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('c', process=True))
68
+
69
+ uid: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('u', process=True))
70
+ login_name: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('L', process=True))
71
+
72
+ # FD is the File Descriptor number of the file or:
73
+ # cwd current working directory;
74
+ # Lnn library references (AIX);
75
+ # ctty character tty;
76
+ # DEL deleted file;
77
+ # err FD information error (see NAME column);
78
+ # fp. Fileport (Darwin);
79
+ # jld jail directory (FreeBSD);
80
+ # ltx shared library text (code and data);
81
+ # Mxx hex memory-mapped type number xx.
82
+ # m86 DOS Merge mapped file;
83
+ # mem memory-mapped file;
84
+ # mmap memory-mapped device;
85
+ # NOFD for a Linux /proc/<PID>/fd directory that can't be opened -- the directory path appears in the NAME column,
86
+ # followed by an error message;
87
+ # pd parent directory;
88
+ # Rnn unknown pregion number (HP-UX);
89
+ # rtd root directory;
90
+ # twd per task current working directory;
91
+ # txt program text (code and data);
92
+ # v86 VP/ix mapped file;
93
+ #
94
+ # FD is followed by one of these characters, describing the mode under which the file is open:
95
+ # r for read access;
96
+ # w for write access;
97
+ # u for read and write access;
98
+ # space if mode unknown and no lock
99
+ # character follows;
100
+ # `-' if mode unknown and lock
101
+ # character follows.
102
+ #
103
+ # The mode character is followed by one of these lock characters, describing the type of lock applied to the file:
104
+ # N for a Solaris NFS lock of unknown type;
105
+ # r for read lock on part of the file;
106
+ # R for a read lock on the entire file;
107
+ # w for a write lock on part of the file;
108
+ # W for a write lock on the entire file;
109
+ # u for a read and write lock of any length;
110
+ # U for a lock of unknown type;
111
+ # x for an SCO OpenServer Xenix lock on part of the file;
112
+ # X for an SCO OpenServer Xenix lock on the entire file;
113
+ # space if there is no lock.
114
+ #
115
+ fd: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('f'))
116
+
117
+ inode: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('i'))
118
+ name: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('n'))
119
+
120
+ # r = read; w = write; u = read/write
121
+ access: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('a'))
122
+
123
+ # r/R = read; w/W = write; u = read/write
124
+ lock: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('l'))
125
+
126
+ file_flags: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('G'))
127
+ file_type: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('t'))
128
+ file_size: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('s'))
129
+ file_offset: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('o')) # as 0t<dec> or 0x<hex>
130
+
131
+ device_character_code: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('d'))
132
+ device_number: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('D')) # as 0x<hex>
133
+ raw_device_number: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('r')) # as 0x<hex>
134
+
135
+ share_count: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('C'))
136
+ link_count: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('k'))
137
+
138
+ stream_info: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('S'))
139
+ protocol_name: ta.Optional[str] = dc.field(default=None, metadata=_LsofFieldMeta.make('P'))
140
+
141
+ tcp_tpi_info: ta.Optional[ta.Sequence[str]] = dc.field(
142
+ default=None,
143
+ metadata=_LsofFieldMeta.make('T', variadic=True),
144
+ )
145
+
146
+ # 'K': 'task_id', # unknown field
147
+ # 'M': 'task_command_name', # unknown field
148
+
149
+ _FIELDS_BY_PREFIX: ta.ClassVar[ta.Mapping[str, dc.Field]]
150
+ _DEFAULT_PREFIXES: ta.ClassVar[str]
151
+
152
+ @classmethod
153
+ def from_prefixes(cls, dct: ta.Mapping[str, ta.Any]) -> 'LsofItem':
154
+ kw: ta.Dict[str, ta.Any] = {
155
+ fld.name: val
156
+ for pfx, val in dct.items()
157
+ if (fld := cls._FIELDS_BY_PREFIX.get(pfx)) is not None
158
+ }
159
+
160
+ return cls(**kw)
161
+
162
+ @classmethod
163
+ def from_prefix_lines(cls, lines: ta.Iterable[str]) -> ta.List['LsofItem']:
164
+ proc_dct: ta.Dict[str, ta.Any] = {}
165
+ file_dct: ta.Dict[str, ta.Any] = {}
166
+ out: ta.List[LsofItem] = []
167
+
168
+ def emit() -> None:
169
+ if file_dct:
170
+ out.append(cls.from_prefixes({**proc_dct, **file_dct}))
171
+
172
+ for line in lines:
173
+ pfx, val = line[0], line[1:]
174
+ try:
175
+ fld = cls._FIELDS_BY_PREFIX[pfx]
176
+ except KeyError:
177
+ continue
178
+ meta: _LsofFieldMeta = fld.metadata[_LsofFieldMeta]
179
+
180
+ if pfx == 'p':
181
+ emit()
182
+ proc_dct = {}
183
+ file_dct = {}
184
+ elif pfx == 'f':
185
+ emit()
186
+ file_dct = {}
187
+
188
+ if meta.process:
189
+ dct = proc_dct
190
+ else:
191
+ dct = file_dct
192
+
193
+ if meta.variadic:
194
+ dct.setdefault(pfx, []).append(val)
195
+ else:
196
+ if pfx in dct:
197
+ raise KeyError(pfx)
198
+ dct[pfx] = val
199
+
200
+ emit()
201
+
202
+ return out
203
+
204
+
205
+ LsofItem._FIELDS_BY_PREFIX = { # noqa
206
+ meta.prefix: fld
207
+ for fld in dc.fields(LsofItem) # noqa
208
+ if (meta := fld.metadata.get(_LsofFieldMeta)) is not None # noqa
209
+ }
210
+
211
+ LsofItem._DEFAULT_PREFIXES = ''.join(LsofItem._FIELDS_BY_PREFIX) # noqa
212
+
213
+
214
+ ##
215
+
216
+
217
+ @dc.dataclass(frozen=True)
218
+ class \
219
+ LsofCommand(SubprocessRunnable[ta.List[LsofItem]]):
220
+ pid: ta.Optional[int] = None
221
+ file: ta.Optional[str] = None
222
+
223
+ prefixes: ta.Optional[ta.Sequence[str]] = None
224
+
225
+ def make_run(self) -> SubprocessRun:
226
+ if (prefixes := self.prefixes) is None:
227
+ prefixes = LsofItem._DEFAULT_PREFIXES # noqa
228
+
229
+ return SubprocessRun.of(
230
+ 'lsof',
231
+ '-F', ''.join(prefixes),
232
+ *(['-p', str(self.pid)] if self.pid is not None else []),
233
+ *([self.file] if self.file is not None else []),
234
+
235
+ stdout='pipe',
236
+ stderr='devnull',
237
+ check=True,
238
+ )
239
+
240
+ def handle_run_output(self, output: SubprocessRunOutput) -> ta.List[LsofItem]:
241
+ lines = [s for l in check.not_none(output.stdout).decode().splitlines() if (s := l.strip())]
242
+ return LsofItem.from_prefix_lines(lines)
243
+
244
+
245
+ if __name__ == '__main__':
246
+ def _main() -> None:
247
+ argparse = __import__('argparse')
248
+ parser = argparse.ArgumentParser()
249
+ parser.add_argument('--pid', '-p', type=int)
250
+ parser.add_argument('file', nargs='?')
251
+ args = parser.parse_args()
252
+
253
+ importlib = __import__('importlib')
254
+ subprocesses = importlib.import_module('..subprocesses.sync', package=__package__).subprocesses
255
+ items = LsofCommand(
256
+ pid=args.pid,
257
+ file=args.file,
258
+ ).run(subprocesses)
259
+
260
+ json = __import__('json')
261
+ marshal_obj = importlib.import_module('..lite.marshal', package=__package__).marshal_obj
262
+ print(json.dumps(marshal_obj(items), indent=2))
263
+
264
+ _main()
omlish/diag/ps.py CHANGED
@@ -1,8 +1,15 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
1
3
  import dataclasses as dc
2
4
  import os
3
- import subprocess
5
+ import typing as ta
4
6
 
5
- from .. import lang
7
+ from ..lite.check import check
8
+ from ..lite.timeouts import Timeout
9
+ from ..subprocesses.run import SubprocessRun
10
+ from ..subprocesses.run import SubprocessRunnable
11
+ from ..subprocesses.run import SubprocessRunOutput
12
+ from ..subprocesses.sync import subprocesses
6
13
 
7
14
 
8
15
  @dc.dataclass(frozen=True)
@@ -12,23 +19,36 @@ class PsItem:
12
19
  cmd: str
13
20
 
14
21
 
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
- )
22
+ @dc.dataclass(frozen=True)
23
+ class PsCommand(SubprocessRunnable):
24
+ pid: ta.Optional[int] = None
25
+
26
+ def make_run(self) -> SubprocessRun:
27
+ return SubprocessRun.of(
28
+ 'ps',
29
+ '-o', 'pid=,ppid=,command=',
30
+ *([str(int(self.pid))] if self.pid is not None else []),
31
+
32
+ check=True,
33
+ stdout='pipe',
34
+ stderr='devnull',
35
+ )
36
+
37
+ def handle_run_output(self, output: SubprocessRunOutput) -> PsItem:
38
+ opid, ppid, cmd = check.not_none(output.stdout).decode().split(maxsplit=2)
39
+ return PsItem(
40
+ int(opid),
41
+ int(ppid),
42
+ cmd.strip(),
43
+ )
44
+
28
45
 
46
+ def get_ps_item(pid: int, timeout: ta.Optional[Timeout] = None) -> PsItem:
47
+ return PsCommand(pid).run(subprocesses, timeout=timeout)
29
48
 
30
- def get_ps_lineage(pid: int, timeout: lang.Timeout | None = None) -> list[PsItem]:
31
- timeout = lang.timeout(timeout)
49
+
50
+ def get_ps_lineage(pid: int, timeout: ta.Optional[Timeout] = None) -> ta.List[PsItem]:
51
+ timeout = Timeout.of(timeout)
32
52
  ret: list[PsItem] = []
33
53
  while True:
34
54
  cur = get_ps_item(pid, timeout)
@@ -39,9 +59,8 @@ def get_ps_lineage(pid: int, timeout: lang.Timeout | None = None) -> list[PsItem
39
59
  return ret
40
60
 
41
61
 
42
- def _main() -> None:
43
- print(get_ps_lineage(os.getpid()))
44
-
45
-
46
62
  if __name__ == '__main__':
63
+ def _main() -> None:
64
+ print(get_ps_lineage(os.getpid()))
65
+
47
66
  _main()
omlish/lang/__init__.py CHANGED
@@ -226,14 +226,6 @@ from .sys import ( # noqa
226
226
  is_gil_enabled,
227
227
  )
228
228
 
229
- from .timeouts import ( # noqa
230
- DeadlineTimeout,
231
- InfiniteTimeout,
232
- Timeout,
233
- TimeoutLike,
234
- timeout,
235
- )
236
-
237
229
  from .typing import ( # noqa
238
230
  BytesLike,
239
231
  SequenceNotStr,
@@ -244,6 +236,13 @@ from .typing import ( # noqa
244
236
 
245
237
  ##
246
238
 
239
+ from ..lite.timeouts import ( # noqa
240
+ DeadlineTimeout,
241
+ InfiniteTimeout,
242
+ Timeout,
243
+ TimeoutLike,
244
+ )
245
+
247
246
  from ..lite.types import ( # noqa
248
247
  BUILTIN_SCALAR_ITERABLE_TYPES,
249
248
  )
@@ -42,3 +42,26 @@ def dataclass_maybe_post_init(sup: ta.Any) -> bool:
42
42
  return False
43
43
  fn()
44
44
  return True
45
+
46
+
47
+ def dataclass_repr_filtered(
48
+ obj: ta.Any,
49
+ fn: ta.Callable[[ta.Any, dc.Field, ta.Any], bool],
50
+ ) -> str:
51
+ return (
52
+ f'{obj.__class__.__qualname__}(' +
53
+ ', '.join([
54
+ f'{f.name}={v!r}'
55
+ for f in dc.fields(obj)
56
+ if fn(obj, f, v := getattr(obj, f.name))
57
+ ]) +
58
+ ')'
59
+ )
60
+
61
+
62
+ def dataclass_repr_omit_none(obj: ta.Any) -> str:
63
+ return dataclass_repr_filtered(obj, lambda o, f, v: v is not None)
64
+
65
+
66
+ def dataclass_repr_omit_falsey(obj: ta.Any) -> str:
67
+ return dataclass_repr_filtered(obj, lambda o, f, v: bool(v))