cs-psutils 20250108__py2.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.
cs/psutils.py ADDED
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/python
2
+ #
3
+
4
+ r'''
5
+ Assorted process and subprocess management functions.
6
+
7
+ Not to be confused with the excellent
8
+ (psutil)[https://pypi.org/project/psutil/] package.
9
+ '''
10
+
11
+ import builtins
12
+ from contextlib import contextmanager
13
+ import errno
14
+ import io
15
+ from itertools import chain
16
+ import logging
17
+ import os
18
+ import shlex
19
+ from signal import SIGTERM, SIGKILL, signal
20
+ from subprocess import DEVNULL as subprocess_DEVNULL, PIPE, Popen, run as subprocess_run
21
+ import sys
22
+ import time
23
+
24
+ from cs.deco import fmtdoc, uses_cmd_options
25
+ from cs.gimmicks import trace, warning, DEVNULL
26
+ from cs.pfx import pfx_call
27
+
28
+ __version__ = '20250108'
29
+
30
+ DISTINFO = {
31
+ 'keywords': ["python2", "python3"],
32
+ 'classifiers': [
33
+ "Programming Language :: Python",
34
+ "Programming Language :: Python :: 3",
35
+ ],
36
+ 'install_requires': [
37
+ 'cs.deco',
38
+ 'cs.gimmicks>=devnull',
39
+ 'cs.pfx',
40
+ ],
41
+ }
42
+
43
+ # maximum number of bytes usable in the argv list for the exec*() functions
44
+ # 262144 below is from MacOS El Capitan "sysctl kern.argmax", then
45
+ # halved because even allowing for the size of the environment this
46
+ # can be too big. Unsure why.
47
+ MAX_ARGV = 262144 // 2
48
+
49
+ def stop(pid, signum=SIGTERM, wait=None, do_SIGKILL=False):
50
+ ''' Stop the process specified by `pid`, optionally await its demise.
51
+
52
+ Parameters:
53
+ * `pid`: process id.
54
+ If `pid` is a string, treat as a process id file and read the
55
+ process id from it.
56
+ * `signum`: the signal to send, default `signal.SIGTERM`.
57
+ * `wait`: whether to wait for the process, default `None`.
58
+ If `None`, return `True` (signal delivered).
59
+ If `0`, wait indefinitely until the process exits as tested by
60
+ `os.kill(pid, 0)`.
61
+ If greater than 0, wait up to `wait` seconds for the process to die;
62
+ if it exits, return `True`, otherwise `False`;
63
+ * `do_SIGKILL`: if true (default `False`),
64
+ send the process `signal.SIGKILL` as a final measure before return.
65
+ '''
66
+ if isinstance(pid, str):
67
+ return stop(int(open(pid, encoding='ascii').read().strip()))
68
+ os.kill(pid, signum)
69
+ if wait is None:
70
+ return True
71
+ assert wait >= 0, "wait (%s) should be >= 0" % (wait,)
72
+ now = time.time()
73
+ then = now + wait
74
+ while True:
75
+ time.sleep(0.1)
76
+ if wait == 0 or time.time() < then:
77
+ try:
78
+ os.kill(pid, 0)
79
+ except OSError as e:
80
+ if e.errno != errno.ESRCH:
81
+ raise
82
+ # process no longer present
83
+ return True
84
+ else:
85
+ if do_SIGKILL:
86
+ try:
87
+ os.kill(pid, SIGKILL)
88
+ except OSError as e:
89
+ if e.errno != errno.ESRCH:
90
+ raise
91
+ return False
92
+
93
+ @contextmanager
94
+ def signal_handler(sig, handler, call_previous=False):
95
+ ''' Context manager to push a new signal handler,
96
+ yielding the old handler,
97
+ restoring the old handler on exit.
98
+ If `call_previous` is true (default `False`)
99
+ also call the old handler after the new handler on receipt of the signal.
100
+
101
+ Parameters:
102
+ * `sig`: the `int` signal number to catch
103
+ * `handler`: the handler function to call with `(sig,frame)`
104
+ * `call_previous`: optional flag (default `False`);
105
+ if true, also call the old handler (if any) after `handler`
106
+ '''
107
+ if call_previous:
108
+ # replace handler() with a wrapper to call both it and the old handler
109
+ handler0 = handler
110
+
111
+ def handler(sig, frame): # pylint:disable=function-redefined
112
+ ''' Call the handler and then the previous handler if requested.
113
+ '''
114
+ handler0(sig, frame)
115
+ if callable(old_handler):
116
+ old_handler(sig, frame)
117
+
118
+ old_handler = signal(sig, handler)
119
+ try:
120
+ yield old_handler
121
+ finally:
122
+ # restiore the previous handler
123
+ signal(sig, old_handler)
124
+
125
+ @contextmanager
126
+ def signal_handlers(sig_hnds, call_previous=False, _stacked=None):
127
+ ''' Context manager to stack multiple signal handlers,
128
+ yielding a mapping of `sig`=>`old_handler`.
129
+
130
+ Parameters:
131
+ * `sig_hnds`: a mapping of `sig`=>`new_handler`
132
+ or an iterable of `(sig,new_handler)` pairs
133
+ * `call_previous`: optional flag (default `False`), passed
134
+ to `signal_handler()`
135
+ '''
136
+ if _stacked is None:
137
+ _stacked = {}
138
+ try:
139
+ items = sig_hnds.items
140
+ except AttributeError:
141
+ # (sig,hnd),... from iterable
142
+ it = iter(sig_hnds)
143
+ else:
144
+ # (sig,hnd),... from mapping
145
+ it = iter(items())
146
+ try:
147
+ sig, handler = next(it)
148
+ except StopIteration:
149
+ pass
150
+ else:
151
+ with signal_handler(sig, handler,
152
+ call_previous=call_previous) as old_handler:
153
+ _stacked[sig] = old_handler
154
+ with signal_handlers(it, call_previous=call_previous,
155
+ _stacked=_stacked) as stacked:
156
+ yield stacked
157
+ return
158
+ yield _stacked
159
+
160
+ def write_pidfile(path, pid=None):
161
+ ''' Write a process id to a pid file.
162
+
163
+ Parameters:
164
+ * `path`: the path to the pid file.
165
+ * `pid`: the process id to write, defautl from `os.getpid`.
166
+ '''
167
+ if pid is None:
168
+ pid = os.getpid()
169
+ with open(path, 'w', encoding='ascii') as pidfp:
170
+ print(pid, file=pidfp)
171
+
172
+ def remove_pidfile(path):
173
+ ''' Truncate and remove a pidfile, permissions permitting.
174
+ '''
175
+ try:
176
+ with open(path, "wb"): # pylint: disable=unspecified-encoding
177
+ pass
178
+ os.remove(path)
179
+ except OSError as e:
180
+ if e.errno != errno.EPERM:
181
+ raise
182
+
183
+ @contextmanager
184
+ def PidFileManager(path, pid=None):
185
+ ''' Context manager for a pid file.
186
+
187
+ Parameters:
188
+ * `path`: the path to the process id file.
189
+ * `pid`: the process id to store in the pid file,
190
+ default from `os.etpid`.
191
+
192
+ Writes the process id file at the start
193
+ and removes the process id file at the end.
194
+ '''
195
+ write_pidfile(path, pid=pid)
196
+ try:
197
+ yield
198
+ finally:
199
+ remove_pidfile(path)
200
+
201
+ @uses_cmd_options(doit=True, quiet=False)
202
+ def run(
203
+ argv,
204
+ *,
205
+ check=True,
206
+ doit: bool,
207
+ input=None,
208
+ logger=None,
209
+ print=None,
210
+ fold=None,
211
+ quiet: bool,
212
+ remote=None,
213
+ ssh_exe=None,
214
+ stdin=None,
215
+ **subp_options,
216
+ ):
217
+ ''' Run a command via `subprocess.run`.
218
+ Return the `CompletedProcess` result or `None` if `doit` is false.
219
+
220
+ Positional parameter:
221
+ * `argv`: the command line to run
222
+
223
+ Note that `argv` is passed through `prep_argv(argv,remote=remote,ssh_exe=ssh_exe)`
224
+ before use, allowing direct invocation with conditional parts.
225
+ See the `prep_argv` function for details.
226
+
227
+ Keyword parameters:
228
+ * `check`: passed to `subprocess.run`, default `True`;
229
+ NB: _unlike_ the `subprocess.run` default, which is `False`
230
+ * `doit`: optional flag, default `True`;
231
+ if false do not run the command and return `None`
232
+ * `fold`: optional flag, passed to `print_argv`
233
+ * `input`: default `None`: alternative to `stdin`;
234
+ passed to `subprocess.run`
235
+ * `logger`: optional logger, default `None`;
236
+ if `True`, use `logging.getLogger()`;
237
+ if not `None` or `False` trace using `print_argv`
238
+ * `quiet`: default `False`; if false, print the command and its output
239
+ * `remote`: optional remote target on which to run `argv`
240
+ * `ssh_exe`: optional command string for the remote shell
241
+ * `stdin`: standard input for the subprocess, default `subprocess.DEVNULL`;
242
+ passed to `subprocess.run`
243
+
244
+ Other keyword parameters are passed to `subprocess.run`.
245
+ '''
246
+ argv = prep_argv(*argv, remote=remote, ssh_exe=ssh_exe)
247
+ if logger is True:
248
+ logger = logging.getLogger()
249
+ if not doit:
250
+ if not quiet:
251
+ if logger:
252
+ trace("skip: %s", shlex.join(argv))
253
+ else:
254
+ if fold is None:
255
+ fold = True
256
+ print_argv(*argv, fold=fold, print=print)
257
+ return None
258
+ if not quiet:
259
+ if logger:
260
+ trace("+ %s", shlex.join(argv))
261
+ else:
262
+ if fold is None:
263
+ fold = False
264
+ print_argv(*argv, indent="+ ", file=sys.stderr, fold=fold, print=print)
265
+ if input is None:
266
+ if stdin is None:
267
+ stdin = subprocess_DEVNULL
268
+ elif stdin is not None:
269
+ raise ValueError("you may not specify both input and stdin")
270
+ cp = pfx_call(
271
+ subprocess_run,
272
+ argv,
273
+ check=check,
274
+ input=input,
275
+ stdin=stdin,
276
+ **subp_options,
277
+ )
278
+ if cp.stderr:
279
+ # TODO: is this a good thing? I have my doubts
280
+ print(" stderr:")
281
+ print(" ", cp.stderr.rstrip().replace("\n", "\n "))
282
+ if cp.returncode != 0:
283
+ warning(
284
+ "run fails, exit code %s from %s",
285
+ cp.returncode,
286
+ shlex.join(cp.args),
287
+ )
288
+ return cp
289
+
290
+ @uses_cmd_options(quiet=False, ssh_exe='ssh')
291
+ def pipefrom(
292
+ argv,
293
+ *,
294
+ quiet: bool,
295
+ remote=None,
296
+ ssh_exe,
297
+ text=True,
298
+ stdin=DEVNULL,
299
+ **popen_kw
300
+ ):
301
+ ''' Pipe text (usually) from a command using `subprocess.Popen`.
302
+ Return the `Popen` object with `.stdout` as a pipe.
303
+
304
+ Parameters:
305
+ * `argv`: the command argument list
306
+ * `quiet`: optional flag, default `False`;
307
+ if true, print the command to `stderr`
308
+ * `text`: optional flag, default `True`; passed to `Popen`.
309
+ * `stdin`: optional value for `Popen`'s `stdin`, default `DEVNULL`
310
+ Other keyword arguments are passed to `Popen`.
311
+
312
+ Note that `argv` is passed through `prep_argv` before use,
313
+ allowing direct invocation with conditional parts.
314
+ See the `prep_argv` function for details.
315
+ '''
316
+ argv = prep_argv(*argv, remote=remote, ssh_exe=ssh_exe)
317
+ if not quiet:
318
+ print_argv(*argv, indent="+ ", end=" |\n", file=sys.stderr)
319
+ return Popen(argv, stdout=PIPE, text=text, stdin=stdin, **popen_kw)
320
+
321
+ # TODO: text= parameter?
322
+ @uses_cmd_options(quiet=False, ssh_exe='ssh')
323
+ def pipeto(argv, *, quiet: bool, remote=None, ssh_exe, **kw):
324
+ ''' Pipe text to a command.
325
+ Optionally trace invocation.
326
+ Return the Popen object with .stdin encoded as text.
327
+
328
+ Parameters:
329
+ * `argv`: the command argument list
330
+ * `trace`: if true (default `False`),
331
+ if `trace` is `True`, recite invocation to stderr
332
+ otherwise presume that `trace` is a stream
333
+ to which to recite the invocation.
334
+
335
+ Other keyword arguments are passed to the `io.TextIOWrapper`
336
+ which wraps the command's input.
337
+
338
+ Note that `argv` is passed through `prep_argv` before use,
339
+ allowing direct invocation with conditional parts.
340
+ See the `prep_argv` function for details.
341
+ '''
342
+ argv = prep_argv(*argv)
343
+ if not quiet:
344
+ print_argv(*argv, indent="| ", file=sys.stderr)
345
+ P = Popen(argv, stdin=PIPE) # pylint: disable=consider-using-with
346
+ P.stdin = io.TextIOWrapper(P.stdin, **kw)
347
+ return P
348
+
349
+ @fmtdoc
350
+ def groupargv(pre_argv, argv, post_argv=(), max_argv=None, encode=False):
351
+ ''' Distribute the array `argv` over multiple arrays
352
+ to fit within `MAX_ARGV`.
353
+ Return a list of argv lists.
354
+
355
+ Parameters:
356
+ * `pre_argv`: the sequence of leading arguments
357
+ * `argv`: the sequence of arguments to distribute; this may not be empty
358
+ * `post_argv`: optional, the sequence of trailing arguments
359
+ * `max_argv`: optional, the maximum length of each distributed
360
+ argument list, default from `MAX_ARGV`: `{MAX_ARGV}`
361
+ * `encode`: default `False`.
362
+ If true, encode the argv sequences into bytes for accurate tallying.
363
+ If `encode` is a Boolean,
364
+ encode the elements with their .encode() method.
365
+ If `encode` is a `str`, encode the elements with their `.encode()`
366
+ method with `encode` as the encoding name;
367
+ otherwise presume that `encode` is a callable
368
+ for encoding each element.
369
+
370
+ The returned argv arrays will contain the encoded element values.
371
+ '''
372
+ if not argv:
373
+ raise ValueError("argv may not be empty")
374
+ if max_argv is None:
375
+ max_argv = MAX_ARGV
376
+ if encode:
377
+ if isinstance(encode, bool):
378
+ pre_argv = [arg.encode() for arg in pre_argv]
379
+ argv = [arg.encode() for arg in argv]
380
+ post_argv = [arg.encode() for arg in post_argv]
381
+ elif isinstance(encode, str):
382
+ pre_argv = [arg.encode(encode) for arg in pre_argv]
383
+ argv = [arg.encode(encode) for arg in argv]
384
+ post_argv = [arg.encode(encode) for arg in post_argv]
385
+ else:
386
+ pre_argv = [encode(arg) for arg in pre_argv]
387
+ argv = [encode(arg) for arg in argv]
388
+ post_argv = [encode(arg) for arg in post_argv]
389
+ else:
390
+ pre_argv = list(pre_argv)
391
+ post_argv = list(post_argv)
392
+ pre_nbytes = sum(len(arg) + 1 for arg in pre_argv)
393
+ post_nbytes = sum(len(arg) + 1 for arg in post_argv)
394
+ argvs = []
395
+ available = max_argv - pre_nbytes - post_nbytes
396
+ per = []
397
+ for arg in argv:
398
+ nbytes = len(arg) + 1
399
+ if available - nbytes < 0:
400
+ if not per:
401
+ raise ValueError(
402
+ "cannot fit argument into argv: available=%d, len(arg)=%d: %r" %
403
+ (available, len(arg), arg)
404
+ )
405
+ argvs.append(pre_argv + per + post_argv)
406
+ available = max_argv - pre_nbytes - post_nbytes
407
+ per = []
408
+ per.append(arg)
409
+ available -= nbytes
410
+ if per:
411
+ argvs.append(pre_argv + per + post_argv)
412
+ return argvs
413
+
414
+ @uses_cmd_options(ssh_exe='ssh')
415
+ def prep_argv(*argv, ssh_exe, remote=None):
416
+ ''' A trite list comprehension to reduce an argument list `*argv`
417
+ to the entries which are not `None` or `False`
418
+ and to flatten other entries which are not strings.
419
+
420
+ This exists ease the construction of argument lists
421
+ with methods like this:
422
+
423
+ >>> command_exe = 'hashindex'
424
+ >>> hashname = 'sha1'
425
+ >>> quiet = False
426
+ >>> verbose = True
427
+ >>> prep_argv(
428
+ ... command_exe,
429
+ ... quiet and '-q',
430
+ ... verbose and '-v',
431
+ ... hashname and ('-h', hashname),
432
+ ... )
433
+ ['hashindex', '-v', '-h', 'sha1']
434
+
435
+ where `verbose` is a `bool` governing the `-v` option
436
+ and `hashname` is either `str` to be passed with `-h hashname`
437
+ or `None` to omit the option.
438
+
439
+ If `remote` is not `None` it is taken to be a remote host on
440
+ which to run `argv`. This is done via the `ssh_exe` argument,
441
+ which defaults to the string `'ssh'`. The value of `ssh_exe`
442
+ is a command string parsed with `shlex.split`. A new `argv`
443
+ is computed as:
444
+
445
+ [
446
+ *shlex.split(ssh_exe),
447
+ remote,
448
+ '--',
449
+ shlex.join(argv),
450
+ ]
451
+ '''
452
+ argv = list(
453
+ chain(
454
+ *[
455
+ ((arg,) if isinstance(arg, str) else arg)
456
+ for arg in argv
457
+ if arg is not None and arg is not False
458
+ ]
459
+ )
460
+ )
461
+ if remote is not None:
462
+ argv = [
463
+ *shlex.split(ssh_exe),
464
+ remote,
465
+ '--',
466
+ shlex.join(argv),
467
+ ]
468
+ return argv
469
+
470
+ def print_argv(
471
+ *argv,
472
+ indent="",
473
+ subindent=" ",
474
+ end="\n",
475
+ file=None,
476
+ fold=False,
477
+ print=None,
478
+ ):
479
+ ''' Print an indented possibly folded command line.
480
+ '''
481
+ if file is None:
482
+ file = sys.stdout
483
+ if print is None:
484
+ print = builtins.print
485
+ pr_argv = []
486
+ was_opt = False
487
+ for i, arg in enumerate(argv):
488
+ if i == 0:
489
+ pr_argv.append(indent)
490
+ was_opt = False
491
+ elif len(arg) >= 2 and arg.startswith('-'):
492
+ if fold:
493
+ # options get a new line
494
+ pr_argv.append(" \\\n" + indent + subindent)
495
+ else:
496
+ pr_argv.append(" ")
497
+ was_opt = True
498
+ else:
499
+ if was_opt:
500
+ pr_argv.append(" ")
501
+ elif fold:
502
+ # nonoptions get a new line
503
+ pr_argv.append(" \\\n" + indent + subindent)
504
+ else:
505
+ pr_argv.append(" ")
506
+ was_opt = False
507
+ pr_argv.append(shlex.quote(arg))
508
+ print(*pr_argv, sep='', end=end, file=file)
509
+
510
+ if __name__ == '__main__':
511
+ for test_max_argv in 64, 20, 16, 8:
512
+ print(
513
+ test_max_argv,
514
+ repr(
515
+ groupargv(
516
+ ['cp', '-a'], ['a', 'bbbb', 'ddddddddddddd'], ['end'],
517
+ max_argv=test_max_argv,
518
+ encode=True
519
+ )
520
+ )
521
+ )
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.3
2
+ Name: cs-psutils
3
+ Version: 20250108
4
+ Summary: Assorted process and subprocess management functions.
5
+ Keywords: python2,python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.deco>=20250103
16
+ Requires-Dist: cs.gimmicks>=20220429
17
+ Requires-Dist: cs.pfx>=20241208
18
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
19
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
20
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
21
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/psutils.py
22
+
23
+ Assorted process and subprocess management functions.
24
+
25
+ *Latest release 20250108*:
26
+ run: accept new optional fold= parameter, plumb to print_argv.
27
+
28
+ Not to be confused with the excellent
29
+ (psutil)[https://pypi.org/project/psutil/] package.
30
+
31
+ ## <a name="groupargv"></a>`groupargv(pre_argv, argv, post_argv=(), max_argv=None, encode=False)`
32
+
33
+ Distribute the array `argv` over multiple arrays
34
+ to fit within `MAX_ARGV`.
35
+ Return a list of argv lists.
36
+
37
+ Parameters:
38
+ * `pre_argv`: the sequence of leading arguments
39
+ * `argv`: the sequence of arguments to distribute; this may not be empty
40
+ * `post_argv`: optional, the sequence of trailing arguments
41
+ * `max_argv`: optional, the maximum length of each distributed
42
+ argument list, default from `MAX_ARGV`: `131072`
43
+ * `encode`: default `False`.
44
+ If true, encode the argv sequences into bytes for accurate tallying.
45
+ If `encode` is a Boolean,
46
+ encode the elements with their .encode() method.
47
+ If `encode` is a `str`, encode the elements with their `.encode()`
48
+ method with `encode` as the encoding name;
49
+ otherwise presume that `encode` is a callable
50
+ for encoding each element.
51
+
52
+ The returned argv arrays will contain the encoded element values.
53
+
54
+ ## <a name="PidFileManager"></a>`PidFileManager(path, pid=None)`
55
+
56
+ Context manager for a pid file.
57
+
58
+ Parameters:
59
+ * `path`: the path to the process id file.
60
+ * `pid`: the process id to store in the pid file,
61
+ default from `os.etpid`.
62
+
63
+ Writes the process id file at the start
64
+ and removes the process id file at the end.
65
+
66
+ ## <a name="pipefrom"></a>`pipefrom(argv, *, quiet: bool, remote=None, ssh_exe, text=True, stdin=-3, **popen_kw)`
67
+
68
+ Pipe text (usually) from a command using `subprocess.Popen`.
69
+ Return the `Popen` object with `.stdout` as a pipe.
70
+
71
+ Parameters:
72
+ * `argv`: the command argument list
73
+ * `quiet`: optional flag, default `False`;
74
+ if true, print the command to `stderr`
75
+ * `text`: optional flag, default `True`; passed to `Popen`.
76
+ * `stdin`: optional value for `Popen`'s `stdin`, default `DEVNULL`
77
+ Other keyword arguments are passed to `Popen`.
78
+
79
+ Note that `argv` is passed through `prep_argv` before use,
80
+ allowing direct invocation with conditional parts.
81
+ See the `prep_argv` function for details.
82
+
83
+ ## <a name="pipeto"></a>`pipeto(argv, *, quiet: bool, remote=None, ssh_exe, **kw)`
84
+
85
+ Pipe text to a command.
86
+ Optionally trace invocation.
87
+ Return the Popen object with .stdin encoded as text.
88
+
89
+ Parameters:
90
+ * `argv`: the command argument list
91
+ * `trace`: if true (default `False`),
92
+ if `trace` is `True`, recite invocation to stderr
93
+ otherwise presume that `trace` is a stream
94
+ to which to recite the invocation.
95
+
96
+ Other keyword arguments are passed to the `io.TextIOWrapper`
97
+ which wraps the command's input.
98
+
99
+ Note that `argv` is passed through `prep_argv` before use,
100
+ allowing direct invocation with conditional parts.
101
+ See the `prep_argv` function for details.
102
+
103
+ ## <a name="prep_argv"></a>`prep_argv(*argv, ssh_exe, remote=None)`
104
+
105
+ A trite list comprehension to reduce an argument list `*argv`
106
+ to the entries which are not `None` or `False`
107
+ and to flatten other entries which are not strings.
108
+
109
+ This exists ease the construction of argument lists
110
+ with methods like this:
111
+
112
+ >>> command_exe = 'hashindex'
113
+ >>> hashname = 'sha1'
114
+ >>> quiet = False
115
+ >>> verbose = True
116
+ >>> prep_argv(
117
+ ... command_exe,
118
+ ... quiet and '-q',
119
+ ... verbose and '-v',
120
+ ... hashname and ('-h', hashname),
121
+ ... )
122
+ ['hashindex', '-v', '-h', 'sha1']
123
+
124
+ where `verbose` is a `bool` governing the `-v` option
125
+ and `hashname` is either `str` to be passed with `-h hashname`
126
+ or `None` to omit the option.
127
+
128
+ If `remote` is not `None` it is taken to be a remote host on
129
+ which to run `argv`. This is done via the `ssh_exe` argument,
130
+ which defaults to the string `'ssh'`. The value of `ssh_exe`
131
+ is a command string parsed with `shlex.split`. A new `argv`
132
+ is computed as:
133
+
134
+ [
135
+ *shlex.split(ssh_exe),
136
+ remote,
137
+ '--',
138
+ shlex.join(argv),
139
+ ]
140
+
141
+ ## <a name="print_argv"></a>`print_argv(*argv, indent='', subindent=' ', end='\n', file=None, fold=False, print=None)`
142
+
143
+ Print an indented possibly folded command line.
144
+
145
+ ## <a name="remove_pidfile"></a>`remove_pidfile(path)`
146
+
147
+ Truncate and remove a pidfile, permissions permitting.
148
+
149
+ ## <a name="run"></a>`run(argv, *, check=True, doit: bool, input=None, logger=None, print=None, fold=None, quiet: bool, remote=None, ssh_exe=None, stdin=None, **subp_options)`
150
+
151
+ Run a command via `subprocess.run`.
152
+ Return the `CompletedProcess` result or `None` if `doit` is false.
153
+
154
+ Positional parameter:
155
+ * `argv`: the command line to run
156
+
157
+ Note that `argv` is passed through `prep_argv(argv,remote=remote,ssh_exe=ssh_exe)`
158
+ before use, allowing direct invocation with conditional parts.
159
+ See the `prep_argv` function for details.
160
+
161
+ Keyword parameters:
162
+ * `check`: passed to `subprocess.run`, default `True`;
163
+ NB: _unlike_ the `subprocess.run` default, which is `False`
164
+ * `doit`: optional flag, default `True`;
165
+ if false do not run the command and return `None`
166
+ * `fold`: optional flag, passed to `print_argv`
167
+ * `input`: default `None`: alternative to `stdin`;
168
+ passed to `subprocess.run`
169
+ * `logger`: optional logger, default `None`;
170
+ if `True`, use `logging.getLogger()`;
171
+ if not `None` or `False` trace using `print_argv`
172
+ * `quiet`: default `False`; if false, print the command and its output
173
+ * `remote`: optional remote target on which to run `argv`
174
+ * `ssh_exe`: optional command string for the remote shell
175
+ * `stdin`: standard input for the subprocess, default `subprocess.DEVNULL`;
176
+ passed to `subprocess.run`
177
+
178
+ Other keyword parameters are passed to `subprocess.run`.
179
+
180
+ ## <a name="signal_handler"></a>`signal_handler(sig, handler, call_previous=False)`
181
+
182
+ Context manager to push a new signal handler,
183
+ yielding the old handler,
184
+ restoring the old handler on exit.
185
+ If `call_previous` is true (default `False`)
186
+ also call the old handler after the new handler on receipt of the signal.
187
+
188
+ Parameters:
189
+ * `sig`: the `int` signal number to catch
190
+ * `handler`: the handler function to call with `(sig,frame)`
191
+ * `call_previous`: optional flag (default `False`);
192
+ if true, also call the old handler (if any) after `handler`
193
+
194
+ ## <a name="signal_handlers"></a>`signal_handlers(sig_hnds, call_previous=False, _stacked=None)`
195
+
196
+ Context manager to stack multiple signal handlers,
197
+ yielding a mapping of `sig`=>`old_handler`.
198
+
199
+ Parameters:
200
+ * `sig_hnds`: a mapping of `sig`=>`new_handler`
201
+ or an iterable of `(sig,new_handler)` pairs
202
+ * `call_previous`: optional flag (default `False`), passed
203
+ to `signal_handler()`
204
+
205
+ ## <a name="stop"></a>`stop(pid, signum=<Signals.SIGTERM: 15>, wait=None, do_SIGKILL=False)`
206
+
207
+ Stop the process specified by `pid`, optionally await its demise.
208
+
209
+ Parameters:
210
+ * `pid`: process id.
211
+ If `pid` is a string, treat as a process id file and read the
212
+ process id from it.
213
+ * `signum`: the signal to send, default `signal.SIGTERM`.
214
+ * `wait`: whether to wait for the process, default `None`.
215
+ If `None`, return `True` (signal delivered).
216
+ If `0`, wait indefinitely until the process exits as tested by
217
+ `os.kill(pid, 0)`.
218
+ If greater than 0, wait up to `wait` seconds for the process to die;
219
+ if it exits, return `True`, otherwise `False`;
220
+ * `do_SIGKILL`: if true (default `False`),
221
+ send the process `signal.SIGKILL` as a final measure before return.
222
+
223
+ ## <a name="write_pidfile"></a>`write_pidfile(path, pid=None)`
224
+
225
+ Write a process id to a pid file.
226
+
227
+ Parameters:
228
+ * `path`: the path to the pid file.
229
+ * `pid`: the process id to write, defautl from `os.getpid`.
230
+
231
+ # Release Log
232
+
233
+
234
+
235
+ *Release 20250108*:
236
+ run: accept new optional fold= parameter, plumb to print_argv.
237
+
238
+ *Release 20241206*:
239
+ run(): accept new remote= and ssh_exe= parameters to support remote execution, default via @uses_cmd_options(ssh_exe).
240
+
241
+ *Release 20241122*:
242
+ * print_argv: new print= parameter to provide a print() function, refactor to use print instead of file.write.
243
+ * run: new optional print parameter, plumb to print_argv.
244
+ * Use @uses_doit and @uses_quiet to provide the default quiet and doit states.
245
+
246
+ *Release 20240316*:
247
+ Fixed release upload artifacts.
248
+
249
+ *Release 20240211*:
250
+ * run: new optional input= parameter to presupply input data.
251
+ * New prep_argv(*argv) function to flatten and trim computes argv lists; use automatically in run(), pipeot(), pipefrom().
252
+
253
+ *Release 20231129*:
254
+ run(): default stdin=subprocess.DEVNULL.
255
+
256
+ *Release 20230612*:
257
+ * pipefrom: default stdin=DEVNULL.
258
+ * Make many parameters keyword only.
259
+
260
+ *Release 20221228*:
261
+ * signal_handlers: bugfix iteration of sig_hnds.
262
+ * Use cs.gimmicks instead of cs.logutils.
263
+ * Drop use of cs.upd, fixes circular import; users of run() may need to call "with Upd().above()" themselves.
264
+
265
+ *Release 20221118*:
266
+ run: do not print cp.stdout.
267
+
268
+ *Release 20220805*:
269
+ run: print trace to stderr.
270
+
271
+ *Release 20220626*:
272
+ run: default quiet=True.
273
+
274
+ *Release 20220606*:
275
+ * run: fold in the superior run() from cs.ebooks.
276
+ * pipefrom,pipeto: replace trace= with quiet= like run().
277
+ * print_argv: add an `end` parameter (used by pipefrom).
278
+
279
+ *Release 20220531*:
280
+ * New print_argv function for writing shell command lines out nicely.
281
+ * Bump requirements for cs.gimmicks.
282
+
283
+ *Release 20220504*:
284
+ signal_handlers: reshape try/except to avoid confusing traceback.
285
+
286
+ *Release 20220429*:
287
+ * New signal_handler(sig,handler,call_previous=False) context manager to push a signal handler.
288
+ * New signal_handlers() context manager to stack handlers for multiple signals.
289
+
290
+ *Release 20190101*:
291
+ Bugfix context manager cleanup. groupargv improvements.
292
+
293
+ *Release 20171112*:
294
+ Bugfix array length counting.
295
+
296
+ *Release 20171110*:
297
+ New function groupargv for breaking up argv lists to fit within the maximum argument limit; constant MAX_ARGV for the default limit.
298
+
299
+ *Release 20171031*:
300
+ run: accept optional pids parameter, a setlike collection of process ids.
301
+
302
+ *Release 20171018*:
303
+ run: replace `trace` parameter with `logger`, default None
304
+
305
+ *Release 20170908.1*:
306
+ remove dependency on cs.pfx
307
+
308
+ *Release 20170908*:
309
+ run: pass extra keyword arguments to subprocess.call
310
+
311
+ *Release 20170906.1*:
312
+ Add run, pipefrom and pipeto - were incorrectly in another module.
313
+
314
+ *Release 20170906*:
315
+ First PyPI release.
@@ -0,0 +1,4 @@
1
+ cs/psutils.py,sha256=-RyWcFNHM1MxARVWWccNAsSWzab-jb9Hf-MmgB60sXc,15911
2
+ cs_psutils-20250108.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
3
+ cs_psutils-20250108.dist-info/METADATA,sha256=_AqLS6YaY_6MpKUCSWz5_mhSwKsI8GWli_t6kEgUcjQ,11184
4
+ cs_psutils-20250108.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.10.1
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any