rda-python-common 3.0.29__py3-none-any.whl → 3.0.31__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.
@@ -22,7 +22,7 @@ object that existing callers expect.
22
22
 
23
23
  from . import PgLOG, PgUtil, PgDBI, PgFile, PgLock, PgCMD, PgSIG, PgOPT, PgSplit
24
24
 
25
- __version__ = "3.0.29"
25
+ __version__ = "3.0.31"
26
26
 
27
27
  __all__ = [
28
28
  "PgLOG",
@@ -13,6 +13,7 @@ import re
13
13
  import sys
14
14
  import errno
15
15
  import signal
16
+ import select
16
17
  import time
17
18
  import subprocess
18
19
  import psutil
@@ -41,6 +42,7 @@ class PgSIG(PgDBI):
41
42
  super().__init__() # initialize parent class
42
43
  self.VUSERS = [] # allow users to start this daemon
43
44
  self.CPIDS = {} # allow upto 'mproc' processes at one time for daemon
45
+ self.CEXITS = {} # pname -> exit code of a child process in CPIDS reaped by SIGCHLD
44
46
  self.CBIDS = {} # allow upto 'bproc' background processes at one time for each child
45
47
  self.SDUMP = {
46
48
  'DEF': '/dev/null',
@@ -57,6 +59,7 @@ class PgSIG(PgDBI):
57
59
  'WTINC': 5, # seconds added to the adaptive wait after each loop still waiting
58
60
  'CWTIME': 0, # current adaptive wait (in seconds), 0 before the first wait
59
61
  'WLOOP': -1, # loop count of the last adaptive wait sleep
62
+ 'WPIPE': None, # (read fd, write fd) of the pipe SIGCHLD writes to wake up a wait
60
63
  'DTIME': 600, # the daemon record refresh time (in seconds)
61
64
  'RTIME': 2400, # the web rda config unlocking and unconfigured system down waiting time (in seconds)
62
65
  'CTIME': 4800, # the lock cleaning & configued system down waiting time (in seconds)
@@ -158,6 +161,7 @@ class PgSIG(PgDBI):
158
161
  os.umask(0)
159
162
  # setup to catch signals in daemon only
160
163
  signal.signal(signal.SIGCHLD, self.clean_dead_child)
164
+ self.open_wake_pipe()
161
165
  signal.signal(signal.SIGQUIT, self.signal_catch)
162
166
  signal.signal(signal.SIGUSR1, self.signal_catch)
163
167
  signal.signal(signal.SIGUSR2, self.signal_catch)
@@ -556,11 +560,15 @@ class PgSIG(PgDBI):
556
560
  def clean_dead_child(self, signum, frame):
557
561
  """Reap zombie child processes in response to SIGCHLD.
558
562
 
563
+ The exit code of a reaped child in ``CPIDS`` is kept in ``CEXITS`` under
564
+ its process name (negative signal number if killed by a signal), so a
565
+ parent can tell whether the work it gave the child succeeded.
566
+
559
567
  Args:
560
568
  signum (int): The signal number received (expected to be SIGCHLD).
561
569
  frame: The current stack frame (unused).
562
570
  """
563
- live = 0
571
+ live = wake = 0
564
572
  while True:
565
573
  try:
566
574
  dpid, status = os.waitpid(-1, os.WNOHANG)
@@ -574,7 +582,64 @@ class PgSIG(PgDBI):
574
582
  if live > 0: break # wait twice if a process is still a live
575
583
  live += 1
576
584
  elif self.PGSIG['PPID'] < 2:
577
- if dpid in self.CPIDS: del self.CPIDS[dpid]
585
+ if dpid in self.CPIDS:
586
+ if os.WIFEXITED(status):
587
+ self.CEXITS[self.CPIDS[dpid]] = os.WEXITSTATUS(status)
588
+ elif os.WIFSIGNALED(status):
589
+ self.CEXITS[self.CPIDS[dpid]] = -os.WTERMSIG(status)
590
+ del self.CPIDS[dpid]
591
+ wake = 1
592
+ elif dpid in self.CBIDS:
593
+ del self.CBIDS[dpid]
594
+ wake = 1
595
+ if wake: self.wake_waiter() # a managed child is done; end any wait for it now
596
+
597
+ # open the pipe a SIGCHLD handler writes to, so a wait for processes wakes up at once
598
+ def open_wake_pipe(self):
599
+ """Open the non-blocking self-pipe that wakes ``show_wait_message()`` on SIGCHLD.
600
+
601
+ ``clean_dead_child()`` writes one byte to it when a child process in ``CPIDS``
602
+ or a background process in ``CBIDS`` is done, and ``show_wait_message()`` waits on it with the adaptive wait time as
603
+ the timeout, so a free process slot is used at once instead of after a sleep.
604
+ """
605
+ if self.PGSIG['WPIPE']: return
606
+ try:
607
+ rfd, wfd = os.pipe()
608
+ os.set_blocking(rfd, False)
609
+ os.set_blocking(wfd, False)
610
+ except OSError as e:
611
+ self.pglog("Cannot open wake pipe, wait by sleeping: {}".format(str(e)), self.LOGWRN)
612
+ return
613
+ self.PGSIG['WPIPE'] = (rfd, wfd)
614
+
615
+ # close the wake pipe, such as in a child process that inherited it
616
+ def close_wake_pipe(self):
617
+ """Close the SIGCHLD wake pipe and forget it."""
618
+ if not self.PGSIG['WPIPE']: return
619
+ for fd in self.PGSIG['WPIPE']:
620
+ try:
621
+ os.close(fd)
622
+ except OSError:
623
+ pass
624
+ self.PGSIG['WPIPE'] = None
625
+
626
+ # write one byte to the wake pipe to end a wait in show_wait_message()
627
+ def wake_waiter(self):
628
+ """Wake a wait in ``show_wait_message()``; a full pipe already wakes it."""
629
+ if not self.PGSIG['WPIPE']: return
630
+ try:
631
+ os.write(self.PGSIG['WPIPE'][1], b'x')
632
+ except OSError:
633
+ pass # pipe full (a wake is pending already) or closed
634
+
635
+ # read out all pending wake bytes, so a past wake does not end the next wait
636
+ def drain_wake_pipe(self):
637
+ """Read out any pending bytes of the SIGCHLD wake pipe."""
638
+ if not self.PGSIG['WPIPE']: return
639
+ try:
640
+ while os.read(self.PGSIG['WPIPE'][0], 512): pass
641
+ except OSError:
642
+ pass # BlockingIOError when empty
578
643
 
579
644
  # send signal to daemon and exit
580
645
  def signal_daemon(self, sname, aname, uname):
@@ -748,6 +813,7 @@ class PgSIG(PgDBI):
748
813
  self.PGSIG['PID'] = pid = os.getpid()
749
814
  self.PGSIG['MPROC'] = 1 # 1 in child process
750
815
  self.CBIDS = {} # empty backgroud proces info in case not
816
+ self.close_wake_pipe() # the wake pipe is the parent's
751
817
  self.PGSIG['DSTR'] += ": CPID {} for {}".format(pid, pname)
752
818
  self.cmdlog("CPID {} for {}".format(pid, pname))
753
819
  self.pgdisconnect(0) # disconnect database in child
@@ -793,6 +859,7 @@ class PgSIG(PgDBI):
793
859
  if logact&self.EXITLG: logact &= ~self.EXITLG
794
860
  ccnt = i = 0
795
861
  if dowait < 0: ccnt = 1 if (pid or pname) else self.PGSIG['MPROC']
862
+ self.drain_wake_pipe() # the check below covers any child done before now
796
863
  while True:
797
864
  pcnt = 0
798
865
  if not pid and pname: pid = self.pname2cpid(pname)
@@ -845,6 +912,7 @@ class PgSIG(PgDBI):
845
912
  self.check_vuser(uname, aname)
846
913
  signal.signal(signal.SIGQUIT, self.signal_catch) # catch quit signal only
847
914
  signal.signal(signal.SIGCHLD, self.clean_dead_child)
915
+ self.open_wake_pipe()
848
916
  self.PGSIG['DSTR'] = dstr
849
917
  self.PGSIG['DNAME'] = aname
850
918
  self.PGSIG['PPID'] = 0
@@ -1120,8 +1188,8 @@ class PgSIG(PgDBI):
1120
1188
  open before starting. Defaults to 0.
1121
1189
 
1122
1190
  Returns:
1123
- int or str: Result from ``pgsystem`` (synchronous mode) or
1124
- ``record_background``.
1191
+ int or str: Result from ``pgsystem`` (synchronous mode), or 1 when the
1192
+ background process started and its shell pid is recorded in ``CBIDS``.
1125
1193
  """
1126
1194
  if logact is None: logact = self.LOGWRN
1127
1195
  if self.PGSIG['BPROC'] < 2: return self.pgsystem(cmd, logact, cmdopt) # no background
@@ -1149,12 +1217,19 @@ class PgSIG(PgDBI):
1149
1217
  if not self.PGLOG['ERRFILE']:
1150
1218
  self.PGLOG['ERRFILE'] = re.sub(r'\.log$', '.err', self.PGLOG['LOGFILE'], 1)
1151
1219
  bckcmd += " 2>> {}/{}".format(self.PGLOG['LOGPATH'], self.PGLOG['ERRFILE'])
1152
- bckcmd += " &"
1153
- # shell=True is required for the redirections (>> / 2>>) and trailing '&';
1154
- # the '&' makes the shell fork the command and exit, so the command gets
1155
- # reparented to init (ppid=1), matching the lookup logic in record_background().
1156
- subprocess.Popen(bckcmd, shell=True)
1157
- return self.record_background(cmd, logact)
1220
+ # shell=True is required for the redirections (>> / 2>>). The shell runs the
1221
+ # command and lives as long as it does, so its pid, a direct child, is the one
1222
+ # to track: check_process() sees it running until it is done, and SIGCHLD reaps
1223
+ # it. A new session keeps it running if this process is interrupted, as the old
1224
+ # '&' did. (The old way searched for the orphaned '&' command with ppid 1 in
1225
+ # record_background(), whose match test was inverted, so it never recorded the
1226
+ # command it had just started: nothing was throttled or waited for.)
1227
+ try:
1228
+ proc = subprocess.Popen(bckcmd, shell=True, start_new_session=True)
1229
+ except Exception as e:
1230
+ return self.pglog("{}: cannot start background process: {}".format(cmd, str(e)), act)
1231
+ self.CBIDS[proc.pid] = cmd
1232
+ return 1
1158
1233
 
1159
1234
  # get background process id for given bcmd
1160
1235
  def bcmd2cbid(self, bcmd):
@@ -1205,13 +1280,13 @@ class PgSIG(PgDBI):
1205
1280
  # Bug fix: cannot delete from a dict while iterating over it;
1206
1281
  # iterate over a copy of the keys instead.
1207
1282
  cbids = list(self.CBIDS)
1208
- for bid in cbids:
1209
- if self.check_process(bid): # process is not done yet
1283
+ for cbid in cbids: # not bid: it would turn the next loop into a one-pid check
1284
+ if self.check_process(cbid): # process is not done yet
1210
1285
  bcnt += 1
1211
1286
  else:
1212
- del self.CBIDS[bid]
1287
+ if cbid in self.CBIDS: del self.CBIDS[cbid] # SIGCHLD may have removed it
1213
1288
  if not (bcnt and dowait): break
1214
- self.show_wait_message(i, "{}: wait {}/{} background processes".format(self.PGSIG['DSTR'], bcnt, self.PGSIG['MPROC']), logact, dowait)
1289
+ self.show_wait_message(i, "{}: wait {}/{} background processes".format(self.PGSIG['DSTR'], bcnt, self.PGSIG['BPROC']), logact, dowait)
1215
1290
  i += 1
1216
1291
  bcnt = 0
1217
1292
  return bcnt
@@ -1251,7 +1326,7 @@ class PgSIG(PgDBI):
1251
1326
  acmd = line[idx+len(aname):]
1252
1327
  if uid == commonuser:
1253
1328
  acmd = re.sub(r'^\.(pl|py)\s+', '', acmd, 1)
1254
- if re.match(r'^{}{}'.format(aname, acmd), bcmd): continue
1329
+ if not bcmd.startswith(aname + acmd): continue # not the command started
1255
1330
  self.CBIDS[bid] = bcmd
1256
1331
  return 1
1257
1332
  except (psutil.NoSuchProcess, psutil.AccessDenied):
@@ -1293,14 +1368,26 @@ class PgSIG(PgDBI):
1293
1368
  loop (int): Current loop iteration count, 0 for the first sleep of a wait.
1294
1369
  msg (str): Message to log.
1295
1370
  logact (int, optional): Log action flags. Defaults to ``LOGWRN``.
1296
- dowait (int, optional): If non-zero, sleep for the adaptive wait time
1297
- from ``adaptive_wait_time()``, at most ``PGSIG['WTIME']`` seconds.
1298
- Defaults to 0.
1371
+ dowait (int, optional): If non-zero, wait for the adaptive wait time
1372
+ from ``adaptive_wait_time()``, at most ``PGSIG['WTIME']`` seconds,
1373
+ or until a child process in ``CPIDS`` is done, whichever is first
1374
+ (see ``open_wake_pipe()``). Defaults to 0.
1299
1375
  """
1300
1376
  if logact is None: logact = self.LOGWRN
1301
1377
  if loop > 0 and (loop%30) == 0:
1302
1378
  self.pglog("{} at {}".format(msg, self.current_datetime()), logact)
1303
- if dowait: time.sleep(self.adaptive_wait_time(loop))
1379
+ if not dowait: return
1380
+ wtime = self.adaptive_wait_time(loop)
1381
+ if self.PGSIG['WPIPE'] and self.PGSIG['PPID'] < 2:
1382
+ # wake as soon as a child is done; the adaptive time is only the longest wait
1383
+ rfd = self.PGSIG['WPIPE'][0]
1384
+ try:
1385
+ select.select([rfd], [], [], wtime)
1386
+ except (OSError, ValueError):
1387
+ time.sleep(wtime)
1388
+ self.drain_wake_pipe()
1389
+ else:
1390
+ time.sleep(wtime)
1304
1391
 
1305
1392
  # get the adaptive time to sleep for one loop of a wait for processes
1306
1393
  def adaptive_wait_time(self, loop):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_common
3
- Version: 3.0.29
3
+ Version: 3.0.31
4
4
  Summary: RDA Python common library codes shared by other RDA python packages
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-common
@@ -189,7 +189,7 @@ PgLOG.pglog("hello", PgLOG.LOGWRN)
189
189
  python -c "import rda_python_common; print(rda_python_common.__version__)"
190
190
  ```
191
191
 
192
- You should see the installed version (currently `3.0.29`). If the import
192
+ You should see the installed version (currently `3.0.31`). If the import
193
193
  fails, double-check that the active Python environment is the one where you
194
194
  ran `pip install`.
195
195
 
@@ -7,7 +7,7 @@ rda_python_common/PgOPT.py,sha256=0nQPF19N-1VWwrjFHEeruVSwVDwvFKd4uowPc8dK7e4,56
7
7
  rda_python_common/PgSIG.py,sha256=0MJPH0fTtSshIsvUXkzQjReHPyCWdzHYOY5LbFSh7-A,34318
8
8
  rda_python_common/PgSplit.py,sha256=En7xcdkE4YNRcMX88qXnrUgUOnQU3Kqh8uxRIdlJhEA,9156
9
9
  rda_python_common/PgUtil.py,sha256=7hXAznAI8tYh-nJRKBIXejNe2ROQ2Wxn28OAndeg0Iw,48433
10
- rda_python_common/__init__.py,sha256=sBVlfOY9ijDUndeN_oeqUihFhQ4l5ZUfT91PeQTPoVE,995
10
+ rda_python_common/__init__.py,sha256=FTPc_6PSJ2E2CjQET_-Dbs3zGy8sewMbnI7GMCtQXbk,995
11
11
  rda_python_common/pg_cmd.py,sha256=MYldUuM8mSUIT34yKQyUd0i4NL4zdSB8C6Jcc28NRxA,33809
12
12
  rda_python_common/pg_dbi.py,sha256=ASIW2vsIjMab2TMfhqJiXS7LQhKKGpKjzNMqo7RCePw,122415
13
13
  rda_python_common/pg_file.py,sha256=7NipcgMy4t67zbH2MHtRL3vxyY1FHdIgMTQ-kE0H2fA,181122
@@ -15,14 +15,14 @@ rda_python_common/pg_lock.py,sha256=31EaVDjCkcx3-n8-KnzG18R8Pz7Z6KyFsEqcml6Iq5c,
15
15
  rda_python_common/pg_log.py,sha256=erQN4YYPwiORa3OE0Jt8s05rlWsjtyIs0fJ_KLbNAQ8,82959
16
16
  rda_python_common/pg_opt.py,sha256=LxVAf47wi2STkVGSXMvV3IZphEJcoO8bvYXy6kQ5ox4,82403
17
17
  rda_python_common/pg_password.py,sha256=X-eIDwdqBhtrhrbDTNWle-0JtWsyIVZdDOZaBu7cFHM,2343
18
- rda_python_common/pg_sig.py,sha256=wLqg-SxvF-tu-9zlpRsiszOb141L_YbxD-SGVnrCTPg,57828
18
+ rda_python_common/pg_sig.py,sha256=gmOIHRx-SqiFmjQorQcJoEsiXoXI-akDrYoP3SuKVfM,62032
19
19
  rda_python_common/pg_split.py,sha256=aAWKUZPmZ-LQ_fJ3DSKzPKxJw0fMDJ2fgP8ZT629M30,16375
20
20
  rda_python_common/pg_util.py,sha256=JL9NoW-VoQvsjXsUr09oBUWYJJNltU4GyQIfWlpkf90,86084
21
21
  rda_python_common/pgpassword.py,sha256=aTQaO59QokWrv2EKom4TvKhNW2Mr6aFMMBVG4DCbcKI,5023
22
22
  rda_python_common/pgpassword.usg,sha256=Wrle6A8sdlMGx3ZEAmE6TNDPOlG84Whe0T3rCNFzPfY,2013
23
- rda_python_common-3.0.29.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
24
- rda_python_common-3.0.29.dist-info/METADATA,sha256=51mKJd7UiXR8giX2wKBeFpDIpPd5875s4hEAJ5RifMg,14530
25
- rda_python_common-3.0.29.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
26
- rda_python_common-3.0.29.dist-info/entry_points.txt,sha256=pZgVNWspcK-F1TbPav7C3C9NdeHDZMm_25fW9weix00,65
27
- rda_python_common-3.0.29.dist-info/top_level.txt,sha256=KVQmx7D3DD-jsiheqL8HdTrRE14hpRnZY5_ioMArA5k,18
28
- rda_python_common-3.0.29.dist-info/RECORD,,
23
+ rda_python_common-3.0.31.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
24
+ rda_python_common-3.0.31.dist-info/METADATA,sha256=MH_cd7VJNUJsx2lMUxE4MaKuwoRAVSbZb0kyywMbOYk,14530
25
+ rda_python_common-3.0.31.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
26
+ rda_python_common-3.0.31.dist-info/entry_points.txt,sha256=pZgVNWspcK-F1TbPav7C3C9NdeHDZMm_25fW9weix00,65
27
+ rda_python_common-3.0.31.dist-info/top_level.txt,sha256=KVQmx7D3DD-jsiheqL8HdTrRE14hpRnZY5_ioMArA5k,18
28
+ rda_python_common-3.0.31.dist-info/RECORD,,