make signal-handler less shit;

previously: threading.Condition to wakeup the actual handler;
exciting chance of heisenbugs / deadlocks (theoretically)

almost went with os.pipe on unix and socketpairs on windows,
but turns out SimpleQueue is perfect and safe for this purpose

SimpleQueue is 3.7+ so use a regular queue on <3.7
(same problems as original approach)

also need dedicated thread for popping the queue on <3.7
to avoid deadlock on most platforms (--sig-thr)

new features:

--logrot-sig sets a signal for immediate log-rotate

--stack-sig sets a signal to dump stack to log/stdout

--reload-sig sets a signal to initiates config-reload
   (was hardcoded to USR1 previously)
This commit is contained in:
ed 2026-05-25 00:37:52 +00:00
parent f23ec5d9f8
commit f4f97b6cc3
3 changed files with 75 additions and 40 deletions

View file

@ -1234,6 +1234,7 @@ def add_general(ap, nc, srvname):
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit") ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)") ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best") ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="num cpu-cores for uploads/downloads (0=all); keeping the default is almost always best")
ap2.add_argument("--reload-sig", metavar="S", type=u, default=("" if ANYWIN else "USR1"), help="reload server config when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR1\033[0m], [\033[32mUSR1\033[0m], [\033[32m10\033[0m]")
ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)") ap2.add_argument("--vc-url", metavar="URL", type=u, default="", help="URL to check for vulnerable versions (default-disabled)")
ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks") ap2.add_argument("--vc-age", metavar="HOURS", type=int, default=3, help="how many hours to wait between vulnerability checks")
ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable") ap2.add_argument("--vc-exit", action="store_true", help="panic and exit if current version is vulnerable")
@ -1706,6 +1707,7 @@ def add_logging(ap):
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)") ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color") ap2.add_argument("--flo", metavar="N", type=int, default=1, help="log format for \033[33m-lo\033[0m; [\033[32m1\033[0m]=classic/colors, [\033[32m2\033[0m]=no-color")
ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m") ap2.add_argument("--rlo", metavar="TXT", type=u, default=".1", help="logrotate counter format; see \033[33m--help-rlo\033[0m")
ap2.add_argument("--logrot-sig", metavar="S", type=u, default="", help="immediately logrotate when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGHUP\033[0m], [\033[32mHUP\033[0m], [\033[32m1\033[0m]")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR") ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR") ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster") ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
@ -1980,10 +1982,15 @@ def add_debug(ap):
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead") ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests") ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
if ANYWIN or sys.version_info < (3, 7):
ap2.add_argument("--sig-thr", action="store_true", default=True, help=argparse.SUPPRESS)
else:
ap2.add_argument("--sig-thr", action="store_true", help="start separate thread for OS-signals (try this if CTRL-C is busted)")
ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind") ap2.add_argument("--rm-sck", action="store_true", help="when listening on unix-sockets, do a basic delete+bind instead of the default atomic bind")
ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks") ap2.add_argument("--srch-dbg", action="store_true", help="explain search processing, and do some extra expensive sanity checks")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") ap2.add_argument("--stackmon", metavar="P,S", type=u, default="", help="write stacktrace to \033[33mP\033[0math every \033[33mS\033[0m second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
ap2.add_argument("--stack-sig", metavar="S", type=u, default="", help="show stacktrace when unix-signal \033[33mS\033[0m is received; examples: [\033[32mSIGUSR2\033[0m], [\033[32mUSR2\033[0m], [\033[32m12\033[0m]")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m") ap2.add_argument("--log-thrs", metavar="SEC", type=float, default=0.0, help="list active threads every \033[33mSEC\033[0m")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files") ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches \033[33mREGEX\033[0m; [\033[32m.\033[0m] (a single dot) = all files")
ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m") ap2.add_argument("--bak-flips", action="store_true", help="[up2k] if a client uploads a bitflipped/corrupted chunk, store a copy according to \033[33m--bf-nc\033[0m and \033[33m--bf-dir\033[0m")

View file

@ -82,6 +82,7 @@ from .util import (
odfusion, odfusion,
pybin, pybin,
read_utf8, read_utf8,
signame2int,
start_log_thrs, start_log_thrs,
start_stackmon, start_stackmon,
termsize, termsize,
@ -107,6 +108,12 @@ if PY2:
else: else:
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
try:
from queue import SimpleQueue
except:
# yuul b. alwright
from queue import Queue as SimpleQueue
VER_IDP_DB = 1 VER_IDP_DB = 1
VER_SESSION_DB = 1 VER_SESSION_DB = 1
@ -140,13 +147,9 @@ class SvcHub(object):
self.logf: Optional[typing.TextIO] = None self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = "" self.logf_base_fn = ""
self.is_dut = False # running in unittest; always False self.is_dut = False # running in unittest; always False
self.stop_req = False
self.stopping = False self.stopping = False
self.stopped = False self.stopped = False
self.reload_req = False
self.reload_mutex = threading.Lock() self.reload_mutex = threading.Lock()
self.stop_cond = threading.Condition()
self.nsigs = 3
self.retcode = 0 self.retcode = 0
self.httpsrv_up = 0 self.httpsrv_up = 0
self.qr_tsz = None self.qr_tsz = None
@ -156,6 +159,12 @@ class SvcHub(object):
self.cmon = 0 self.cmon = 0
self.tstack = 0.0 self.tstack = 0.0
self.sig_logrot = -999
self.sig_reload = -999
self.sig_stack = -999
self.nsigs = 7
self.sig = SimpleQueue()
self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8)
if args.sss or args.s >= 3: if args.sss or args.s >= 3:
@ -1451,31 +1460,37 @@ class SvcHub(object):
sigs = [signal.SIGINT, signal.SIGTERM] sigs = [signal.SIGINT, signal.SIGTERM]
if not ANYWIN: if not ANYWIN:
sigs.append(signal.SIGUSR1) sigs.append(signal.SIGHUP)
for (opt, mem) in (
("logrot_sig", "sig_logrot"),
("reload_sig", "sig_reload"),
("stack_sig", "sig_stack"),
):
zs = getattr(self.args, opt)
if not zs:
continue
zi = signame2int(zs)
setattr(self, mem, zi)
sigs.append(zi)
for sig in sigs: for sig in sigs:
signal.signal(sig, self.signal_handler) signal.signal(sig, self.signal_handler)
# macos hangs after shutdown on sigterm with while-sleep, if self.args.sig_thr:
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??) Daemon(self._signal_thr, "svchub-sig")
# linux is fine with both,
# never lucky
if ANYWIN:
# msys-python probably fine but >msys-python
Daemon(self.stop_thr, "svchub-sig")
try: try:
while not self.stop_req: while not self.stopping:
time.sleep(1) time.sleep(1)
except: except:
pass pass
self.shutdown()
# cant join; eats signals on win10 # cant join; eats signals on win10
while not self.stopped: while not self.stopped:
time.sleep(0.1) time.sleep(0.1)
else: else:
self.stop_thr() self._signal_thr()
def start_zeroconf(self) -> None: def start_zeroconf(self) -> None:
self.zc_ngen += 1 self.zc_ngen += 1
@ -1533,17 +1548,6 @@ class SvcHub(object):
self.asrv.load_sessions(True) self.asrv.load_sessions(True)
self.broker.reload_sessions() self.broker.reload_sessions()
def stop_thr(self) -> None:
while not self.stop_req:
with self.stop_cond:
self.stop_cond.wait(9001)
if self.reload_req:
self.reload_req = False
self.reload(True, True)
self.shutdown()
def kill9(self, delay: float = 0.0) -> None: def kill9(self, delay: float = 0.0) -> None:
if delay > 0.01: if delay > 0.01:
time.sleep(delay) time.sleep(delay)
@ -1556,26 +1560,42 @@ class SvcHub(object):
os.kill(os.getpid(), signal.SIGKILL) os.kill(os.getpid(), signal.SIGKILL)
def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None: def signal_handler(self, sig: int, frame: Optional[FrameType]) -> None:
if self.stopping: if sig in (signal.SIGINT, signal.SIGTERM):
if self.nsigs <= 0: self.nsigs -= 1
if self.nsigs == 0:
try: try:
threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start() threading.Thread(target=self.pr, args=("OMBO BREAKER",)).start()
time.sleep(0.1) time.sleep(0.1)
except: except:
pass pass
if self.nsigs <= 0:
self.kill9() self.kill9()
else:
self.nsigs -= 1
return
if not ANYWIN and sig == signal.SIGUSR1: self.sig.put(sig)
self.reload_req = True
def _signal_thr(self) -> None:
while not self.stopping:
sig = self.sig.get()
self._signal_handler(sig)
def _signal_handler(self, sig: int) -> None:
if sig == self.sig_logrot:
self.log("root", "signal: logrotate")
dt = datetime.now(self.tz)
self.logf_base_fn = "\t"
self._set_next_day(dt)
elif sig == self.sig_reload:
self.log("root", "signal: reload")
self.reload(True, True)
elif sig == self.sig_stack:
self.log("root", "signal: stack%s" % (alltrace(),))
else: else:
self.stop_req = True self.shutdown()
with self.stop_cond:
self.stop_cond.notify_all()
def shutdown(self) -> None: def shutdown(self) -> None:
if self.stopping: if self.stopping:
@ -1583,10 +1603,8 @@ class SvcHub(object):
# start_log_thrs(print, 0.1, 1) # start_log_thrs(print, 0.1, 1)
self.nsigs = 3
self.stopping = True self.stopping = True
self.stop_req = True
with self.stop_cond:
self.stop_cond.notify_all()
ret = 1 ret = 1
try: try:

View file

@ -1635,6 +1635,16 @@ def expand_osenv_cs(txt) -> str:
raise Exception(t) raise Exception(t)
def signame2int(txt: str) -> int:
try:
return int(txt)
except:
txt = txt.upper()
if not txt.startswith("SIG"):
txt = "SIG" + txt
return int(getattr(signal, txt))
def rice_tid() -> str: def rice_tid() -> str:
tid = threading.current_thread().ident tid = threading.current_thread().ident
c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:]) c = sunpack(b"B" * 5, spack(b">Q", tid)[-5:])