feat: point the fleet at wss LiveKit room uwh-telhai
Publish and subscribe over wss://livekit.uni-wh.de:7800 and refuse cleartext ws://. Conference room is uwh-telhai. Includes the uncommitted encoded H.264 publish path, Rally hairpin, and KMS wall overlay.
This commit is contained in:
@@ -21,19 +21,183 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from config import load_config, _load_dotenv
|
||||
from discovery import discover_cameras, select_cameras
|
||||
from discovery import Camera, discover_cameras, select_cameras
|
||||
from latency import effective_enhance_mode
|
||||
from participant_tags import token_attributes
|
||||
from rally import discover_rally
|
||||
from tokens import make_token
|
||||
|
||||
log = logging.getLogger("cameras.main")
|
||||
|
||||
|
||||
def worker_args_for(cfg, cam) -> list[str]:
|
||||
return [
|
||||
"--camera-json", json.dumps(cam.__dict__),
|
||||
"--room", cfg.room,
|
||||
def spawn_enhance_daemon(cfg) -> subprocess.Popen:
|
||||
from enhance_daemon import wait_for_socket
|
||||
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "enhance_daemon.py"),
|
||||
"--socket", cfg.enhance_socket,
|
||||
"--enhance-mode", "off" if cfg.enhance_mode == "off" else "force",
|
||||
"--enhance-model", cfg.enhance_model,
|
||||
"--enhance-chunk-s", str(cfg.enhance_chunk_s),
|
||||
"--enhance-hop-s", str(cfg.enhance_hop_s),
|
||||
"--enhance-infer", cfg.enhance_infer,
|
||||
"--beamform-mode", cfg.beamform_mode,
|
||||
"--torch-num-threads", str(max(2, cfg.torch_num_threads)),
|
||||
"--tdoa-every", str(cfg.beamform_tdoa_every),
|
||||
"--audio-rate", str(cfg.audio_rate),
|
||||
]
|
||||
log.info("starting shared enhance daemon at %s", cfg.enhance_socket)
|
||||
proc = subprocess.Popen(cmd)
|
||||
if not wait_for_socket(cfg.enhance_socket, timeout=90):
|
||||
proc.kill()
|
||||
raise RuntimeError(
|
||||
f"enhance daemon did not listen on {cfg.enhance_socket}")
|
||||
log.info("enhance daemon ready pid=%s", proc.pid)
|
||||
return proc
|
||||
|
||||
|
||||
def spawn_capture_daemon(cfg, cameras) -> subprocess.Popen:
|
||||
from frame_shm import wait_for_ready
|
||||
|
||||
payload = [{"identity": c.identity, "video_device": c.video_device}
|
||||
for c in cameras]
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "capture_daemon.py"),
|
||||
"--cameras-json", json.dumps(payload),
|
||||
"--width", str(cfg.width),
|
||||
"--height", str(cfg.height),
|
||||
"--fps", str(cfg.fps),
|
||||
"--shm-dir", cfg.capture_shm_dir,
|
||||
"--vaapi-device", cfg.vaapi_device,
|
||||
]
|
||||
log.info("starting shared capture daemon shm=%s cams=%d",
|
||||
cfg.capture_shm_dir, len(payload))
|
||||
env = dict(os.environ)
|
||||
env["GST_REGISTRY_UPDATE"] = "no"
|
||||
env.setdefault("LIBVA_DRM_DEVICE", cfg.vaapi_device)
|
||||
env.setdefault("LIBVA_DRIVER_NAME", "iHD")
|
||||
proc = subprocess.Popen(cmd, env=env)
|
||||
ready = str(Path(cfg.capture_shm_dir) / "ready")
|
||||
if not wait_for_ready(ready, timeout=30):
|
||||
proc.kill()
|
||||
raise RuntimeError(f"capture daemon did not become ready at {ready}")
|
||||
log.info("capture daemon ready pid=%s", proc.pid)
|
||||
return proc
|
||||
|
||||
|
||||
def spawn_audio_daemon(cfg, cameras) -> subprocess.Popen:
|
||||
from frame_shm import wait_for_ready
|
||||
|
||||
payload = [{"identity": c.identity, "audio_card": c.audio_card}
|
||||
for c in cameras if c.audio_card is not None]
|
||||
if not payload:
|
||||
raise RuntimeError("no cameras with audio cards")
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "audio_daemon.py"),
|
||||
"--cameras-json", json.dumps(payload),
|
||||
"--rate", str(cfg.audio_rate),
|
||||
"--hop-s", str(cfg.enhance_hop_s),
|
||||
"--shm-dir", cfg.audio_shm_dir,
|
||||
]
|
||||
log.info("starting shared audio daemon shm=%s mics=%d",
|
||||
cfg.audio_shm_dir, len(payload))
|
||||
proc = subprocess.Popen(cmd)
|
||||
ready = str(Path(cfg.audio_shm_dir) / "ready")
|
||||
if not wait_for_ready(ready, timeout=30):
|
||||
proc.kill()
|
||||
raise RuntimeError(f"audio daemon did not become ready at {ready}")
|
||||
log.info("audio daemon ready pid=%s", proc.pid)
|
||||
return proc
|
||||
|
||||
|
||||
def spawn_encode_daemon(cfg, cameras, portrait_dir: str = "") -> subprocess.Popen:
|
||||
from frame_shm import wait_for_ready
|
||||
|
||||
payload = []
|
||||
for c in cameras:
|
||||
payload.append({
|
||||
"identity": c.identity,
|
||||
"width": int(c.width or cfg.width),
|
||||
"height": int(c.height or cfg.height),
|
||||
})
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "encode_daemon.py"),
|
||||
"--cameras-json", json.dumps(payload),
|
||||
"--i420-dir", cfg.capture_shm_dir,
|
||||
"--h264-dir", cfg.encode_shm_dir,
|
||||
"--fps", str(cfg.fps),
|
||||
"--bitrate", str(cfg.video_bitrate),
|
||||
"--hi-bitrate", str(cfg.rally_video_bitrate),
|
||||
"--vaapi-device", cfg.vaapi_device,
|
||||
"--codec", cfg.video_codec,
|
||||
]
|
||||
if portrait_dir:
|
||||
cmd.extend(["--portrait-dir", portrait_dir])
|
||||
log.info("starting encode daemon h264=%s cams=%d codec=%s portrait=%s",
|
||||
cfg.encode_shm_dir, len(payload), cfg.video_codec,
|
||||
portrait_dir or "off")
|
||||
env = dict(os.environ)
|
||||
env["GST_REGISTRY_UPDATE"] = "no"
|
||||
env.setdefault("LIBVA_DRM_DEVICE", cfg.vaapi_device)
|
||||
env.setdefault("LIBVA_DRIVER_NAME", "iHD")
|
||||
proc = subprocess.Popen(cmd, env=env)
|
||||
ready = str(Path(cfg.encode_shm_dir) / "ready")
|
||||
if not wait_for_ready(ready, timeout=60):
|
||||
proc.kill()
|
||||
raise RuntimeError(f"encode daemon did not become ready at {ready}")
|
||||
log.info("encode daemon ready pid=%s", proc.pid)
|
||||
return proc
|
||||
|
||||
|
||||
def spawn_portrait_daemon(cfg, cameras) -> subprocess.Popen:
|
||||
from frame_shm import wait_for_ready
|
||||
|
||||
payload = []
|
||||
for c in cameras:
|
||||
if c.identity == "rally":
|
||||
continue
|
||||
payload.append({
|
||||
"identity": c.identity,
|
||||
"width": int(c.width or cfg.width),
|
||||
"height": int(c.height or cfg.height),
|
||||
})
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "portrait_daemon.py"),
|
||||
"--cameras-json", json.dumps(payload),
|
||||
"--in-dir", cfg.capture_shm_dir,
|
||||
"--out-dir", cfg.portrait_shm_dir,
|
||||
"--hz", str(cfg.portrait_hz),
|
||||
"--blur-px", str(cfg.portrait_blur_px),
|
||||
"--hold-s", str(cfg.portrait_hold_s),
|
||||
]
|
||||
log.info("starting portrait daemon shm=%s cams=%d hz=%.1f",
|
||||
cfg.portrait_shm_dir, len(payload), cfg.portrait_hz)
|
||||
env = dict(os.environ)
|
||||
env["OMP_NUM_THREADS"] = "1"
|
||||
env["OPENBLAS_NUM_THREADS"] = "1"
|
||||
env["MKL_NUM_THREADS"] = "1"
|
||||
proc = subprocess.Popen(cmd, env=env)
|
||||
ready = str(Path(cfg.portrait_shm_dir) / "ready")
|
||||
if not wait_for_ready(ready, timeout=45):
|
||||
proc.kill()
|
||||
raise RuntimeError(f"portrait daemon did not become ready at {ready}")
|
||||
log.info("portrait daemon ready pid=%s", proc.pid)
|
||||
return proc
|
||||
|
||||
|
||||
def publisher_args_for(cfg, cameras) -> list[str]:
|
||||
payload = []
|
||||
for cam in cameras:
|
||||
d = dict(cam.__dict__)
|
||||
d["token"] = make_token(
|
||||
cfg.url, cfg.api_key, cfg.api_secret,
|
||||
cfg.room, cam.identity,
|
||||
attributes=token_attributes(cfg.participant_tags))
|
||||
payload.append(d)
|
||||
args = [
|
||||
"--cameras-json", json.dumps(payload),
|
||||
"--url", cfg.url,
|
||||
"--token", make_token(cfg.url, cfg.api_key, cfg.api_secret,
|
||||
cfg.room, cam.identity),
|
||||
"--room", cfg.room,
|
||||
"--width", str(cfg.width),
|
||||
"--height", str(cfg.height),
|
||||
"--fps", str(cfg.fps),
|
||||
@@ -50,7 +214,128 @@ def worker_args_for(cfg, cam) -> list[str]:
|
||||
"--beamform-mode", cfg.beamform_mode,
|
||||
"--torch-num-threads", str(cfg.torch_num_threads),
|
||||
"--tdoa-every", str(cfg.beamform_tdoa_every),
|
||||
"--video-shm-dir", cfg.capture_shm_dir,
|
||||
"--audio-shm-dir", cfg.audio_shm_dir,
|
||||
"--h264-shm-dir", cfg.encode_shm_dir,
|
||||
"--speaker-camera", cfg.display_speaker_camera,
|
||||
"--audio-gate-open-db", str(cfg.audio_gate_open_db),
|
||||
"--audio-gate-close-db", str(cfg.audio_gate_close_db),
|
||||
"--audio-gate-hold-s", str(cfg.audio_gate_hold_s),
|
||||
]
|
||||
if cfg.audio_queue_ms is not None:
|
||||
args.extend(["--audio-queue-ms", str(cfg.audio_queue_ms)])
|
||||
if cfg.enhance_daemon and cfg.enhance_socket:
|
||||
args.extend(["--enhance-socket", cfg.enhance_socket])
|
||||
if cfg.enhance_speaker_only:
|
||||
args.append("--enhance-speaker-only")
|
||||
if cfg.audio_gate:
|
||||
args.append("--audio-gate")
|
||||
return args
|
||||
|
||||
|
||||
def fleet_cameras(cameras: list[Camera]) -> list[Camera]:
|
||||
return [c for c in cameras if c.identity != "rally"]
|
||||
|
||||
|
||||
def spawn_rally_follow(cfg, cam: Camera) -> subprocess.Popen:
|
||||
cmd = [
|
||||
sys.executable, str(Path(__file__).parent / "rally_follow.py"),
|
||||
"--device", cam.video_device,
|
||||
"--width", str(cam.width or 1920),
|
||||
"--height", str(cam.height or 1080),
|
||||
"--fps", "30",
|
||||
"--shm-dir", cfg.capture_shm_dir,
|
||||
"--audio-shm-dir", cfg.audio_shm_dir,
|
||||
"--identity", cam.identity,
|
||||
"--audio-rate", str(cfg.audio_rate),
|
||||
"--hop-s", str(cfg.enhance_hop_s),
|
||||
]
|
||||
if cam.audio_card is not None:
|
||||
cmd.extend(["--audio-card", str(cam.audio_card)])
|
||||
log.info("starting rally follow video=%s audio=%s (preview=LiveKit hairpin)",
|
||||
cam.video_device,
|
||||
f"hw:{cam.audio_card},0" if cam.audio_card is not None else "none")
|
||||
return subprocess.Popen(cmd)
|
||||
|
||||
|
||||
def spawn_publisher(cfg, cameras) -> subprocess.Popen:
|
||||
args = publisher_args_for(cfg, cameras)
|
||||
env = dict(os.environ)
|
||||
env.setdefault("LIBVA_DRIVER_NAME", "iHD")
|
||||
env.setdefault("LIBVA_DRM_DEVICE", cfg.vaapi_device)
|
||||
log.info("starting single publisher cams=%d", len(cameras))
|
||||
return subprocess.Popen(
|
||||
[sys.executable, str(Path(__file__).parent / "run_publisher.py"), *args],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1, env=env,
|
||||
)
|
||||
|
||||
|
||||
def stop_proc(proc: subprocess.Popen | None) -> None:
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=10)
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
|
||||
|
||||
def worker_args_for(cfg, cam) -> list[str]:
|
||||
enhance_mode = effective_enhance_mode(
|
||||
cfg.enhance_mode,
|
||||
cam.identity,
|
||||
cfg.enhance_speaker_only,
|
||||
cfg.display_speaker_camera,
|
||||
)
|
||||
beamform_mode = cfg.beamform_mode
|
||||
if cfg.enhance_speaker_only and enhance_mode == "off":
|
||||
beamform_mode = "off"
|
||||
args = [
|
||||
"--camera-json", json.dumps(cam.__dict__),
|
||||
"--room", cfg.room,
|
||||
"--url", cfg.url,
|
||||
"--token", make_token(
|
||||
cfg.url, cfg.api_key, cfg.api_secret,
|
||||
cfg.room, cam.identity,
|
||||
attributes=token_attributes(cfg.participant_tags)),
|
||||
"--width", str(cfg.width),
|
||||
"--height", str(cfg.height),
|
||||
"--fps", str(cfg.fps),
|
||||
"--bitrate", str(cfg.video_bitrate),
|
||||
"--video-codec", cfg.video_codec,
|
||||
"--video-encoder", cfg.video_encoder,
|
||||
"--vaapi-device", cfg.vaapi_device,
|
||||
"--audio-rate", str(cfg.audio_rate),
|
||||
"--enhance-mode", enhance_mode,
|
||||
"--enhance-model", cfg.enhance_model,
|
||||
"--enhance-chunk-s", str(cfg.enhance_chunk_s),
|
||||
"--enhance-hop-s", str(cfg.enhance_hop_s),
|
||||
"--enhance-infer", cfg.enhance_infer,
|
||||
"--beamform-mode", beamform_mode,
|
||||
"--torch-num-threads", str(cfg.torch_num_threads),
|
||||
"--tdoa-every", str(cfg.beamform_tdoa_every),
|
||||
"--speaker-camera", cfg.display_speaker_camera,
|
||||
"--audio-gate-open-db", str(cfg.audio_gate_open_db),
|
||||
"--audio-gate-close-db", str(cfg.audio_gate_close_db),
|
||||
"--audio-gate-hold-s", str(cfg.audio_gate_hold_s),
|
||||
]
|
||||
if cfg.audio_queue_ms is not None:
|
||||
args.extend(["--audio-queue-ms", str(cfg.audio_queue_ms)])
|
||||
if cfg.video_hold_s is not None:
|
||||
args.extend(["--video-hold-s", str(cfg.video_hold_s)])
|
||||
if cfg.worker_split_executor:
|
||||
args.append("--split-executor")
|
||||
if cfg.enhance_daemon and cfg.enhance_socket:
|
||||
args.extend(["--enhance-socket", cfg.enhance_socket])
|
||||
if cfg.audio_gate:
|
||||
args.append("--audio-gate")
|
||||
if cfg.capture_daemon:
|
||||
args.extend([
|
||||
"--video-shm",
|
||||
f"{cfg.capture_shm_dir.rstrip('/')}/{cam.identity}.i420",
|
||||
])
|
||||
return args
|
||||
|
||||
|
||||
class Supervisor:
|
||||
@@ -128,6 +413,10 @@ def main() -> int:
|
||||
log.error("LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET "
|
||||
"not set (create .env from .env.example)")
|
||||
return 2
|
||||
if not cfg.url.startswith("wss://"):
|
||||
log.error("LIVEKIT_URL must be wss:// (refusing %s)", cfg.url)
|
||||
return 2
|
||||
log.info("livekit destination url=%s room=%s", cfg.url, cfg.room)
|
||||
|
||||
cameras = discover_cameras()
|
||||
log.info("discovered %d cameras:", len(cameras))
|
||||
@@ -138,41 +427,101 @@ def main() -> int:
|
||||
c.usb_port or "-")
|
||||
|
||||
cameras = select_cameras(cameras, cfg.cameras)
|
||||
if cfg.cameras == ["all"]:
|
||||
rally = discover_rally()
|
||||
if rally is not None:
|
||||
cameras = list(cameras) + [rally]
|
||||
log.info(" %s video=%s audio=hw:%s usb=%s",
|
||||
rally.identity, rally.video_device,
|
||||
rally.audio_card if rally.audio_card is not None else "-",
|
||||
rally.usb_port or "-")
|
||||
if not cameras:
|
||||
log.error("no cameras match --cameras %s", args.cameras)
|
||||
return 2
|
||||
|
||||
fleet = fleet_cameras(cameras)
|
||||
daemon = None
|
||||
capture = None
|
||||
audio = None
|
||||
encode = None
|
||||
rally_proc = None
|
||||
portrait = None
|
||||
if cfg.enhance_daemon and cfg.enhance_mode != "off":
|
||||
try:
|
||||
daemon = spawn_enhance_daemon(cfg)
|
||||
except Exception as exc:
|
||||
log.error("enhance daemon failed: %s", exc)
|
||||
return 2
|
||||
if cfg.capture_daemon:
|
||||
try:
|
||||
capture = spawn_capture_daemon(cfg, fleet)
|
||||
except Exception as exc:
|
||||
log.error("capture daemon failed: %s", exc)
|
||||
stop_proc(daemon)
|
||||
return 2
|
||||
if cfg.audio_daemon:
|
||||
try:
|
||||
audio = spawn_audio_daemon(cfg, fleet)
|
||||
except Exception as exc:
|
||||
log.error("audio daemon failed: %s", exc)
|
||||
stop_proc(capture)
|
||||
stop_proc(daemon)
|
||||
return 2
|
||||
rally_cam = next((c for c in cameras if c.identity == "rally"), None)
|
||||
if rally_cam is not None:
|
||||
try:
|
||||
rally_proc = spawn_rally_follow(cfg, rally_cam)
|
||||
shm = Path(cfg.capture_shm_dir) / "rally.i420"
|
||||
from frame_shm import wait_for_ready
|
||||
if not wait_for_ready(str(shm), timeout=20):
|
||||
raise RuntimeError(f"rally follow did not create {shm}")
|
||||
except Exception as exc:
|
||||
log.error("rally follow failed: %s", exc)
|
||||
stop_proc(rally_proc)
|
||||
stop_proc(audio)
|
||||
stop_proc(capture)
|
||||
stop_proc(daemon)
|
||||
return 2
|
||||
portrait_dir = ""
|
||||
if cfg.portrait_blur and cfg.capture_daemon:
|
||||
try:
|
||||
portrait = spawn_portrait_daemon(cfg, fleet)
|
||||
portrait_dir = cfg.portrait_shm_dir
|
||||
except Exception as exc:
|
||||
log.error("portrait daemon failed, encode uses raw: %s", exc)
|
||||
stop_proc(portrait)
|
||||
portrait = None
|
||||
portrait_dir = ""
|
||||
if cfg.encode_daemon and cfg.capture_daemon:
|
||||
try:
|
||||
encode = spawn_encode_daemon(cfg, cameras, portrait_dir=portrait_dir)
|
||||
except Exception as exc:
|
||||
log.error("encode daemon failed: %s", exc)
|
||||
stop_proc(portrait)
|
||||
stop_proc(rally_proc)
|
||||
stop_proc(audio)
|
||||
stop_proc(capture)
|
||||
stop_proc(daemon)
|
||||
return 2
|
||||
|
||||
use_pub = cfg.single_publisher and cfg.capture_daemon
|
||||
sup = Supervisor(cfg, cameras)
|
||||
for cam in cameras:
|
||||
sup.spawn(cam)
|
||||
log.info("spawned %d workers", len(cameras))
|
||||
pub = None
|
||||
if use_pub:
|
||||
pub = spawn_publisher(cfg, cameras)
|
||||
log.info("spawned single publisher for %d cameras", len(cameras))
|
||||
else:
|
||||
for cam in cameras:
|
||||
sup.spawn(cam)
|
||||
log.info("spawned %d workers", len(cameras))
|
||||
|
||||
if args.once:
|
||||
deadline = time.monotonic() + cfg.publish_timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
sup.pump()
|
||||
sup.reap()
|
||||
time.sleep(0.5)
|
||||
# final report
|
||||
up = [i for i, p in sup.procs.items() if p.poll() is None]
|
||||
log.info("STATUS: %d/%d workers alive", len(up), len(sup.procs))
|
||||
for ident in up:
|
||||
pass
|
||||
# stop everything
|
||||
for p in sup.procs.values():
|
||||
p.send_signal(signal.SIGTERM)
|
||||
for p in sup.procs.values():
|
||||
def _shutdown():
|
||||
if pub is not None and pub.poll() is None:
|
||||
pub.send_signal(signal.SIGTERM)
|
||||
with contextlib.suppress(Exception):
|
||||
p.wait(timeout=10)
|
||||
return 0 if len(up) == len(sup.procs) else 1
|
||||
|
||||
try:
|
||||
while True:
|
||||
sup.pump()
|
||||
sup.reap()
|
||||
time.sleep(0.2)
|
||||
except KeyboardInterrupt:
|
||||
log.info("shutting down ...")
|
||||
pub.wait(timeout=10)
|
||||
if pub.poll() is None:
|
||||
pub.kill()
|
||||
for p in sup.procs.values():
|
||||
if p.poll() is None:
|
||||
p.send_signal(signal.SIGTERM)
|
||||
@@ -182,6 +531,57 @@ def main() -> int:
|
||||
for p in sup.procs.values():
|
||||
if p.poll() is None:
|
||||
p.kill()
|
||||
stop_proc(daemon)
|
||||
stop_proc(capture)
|
||||
stop_proc(audio)
|
||||
stop_proc(encode)
|
||||
stop_proc(portrait)
|
||||
stop_proc(rally_proc)
|
||||
|
||||
def _pump_pub():
|
||||
nonlocal pub
|
||||
if pub is None:
|
||||
return
|
||||
line = pub.stdout.readline() if pub.stdout else ""
|
||||
if line:
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
rc = pub.poll()
|
||||
if rc is not None:
|
||||
log.warning("publisher exited rc=%s; restarting", rc)
|
||||
time.sleep(1.0)
|
||||
pub = spawn_publisher(cfg, cameras)
|
||||
|
||||
if args.once:
|
||||
deadline = time.monotonic() + cfg.publish_timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
if use_pub:
|
||||
_pump_pub()
|
||||
else:
|
||||
sup.pump()
|
||||
sup.reap()
|
||||
time.sleep(0.5)
|
||||
if use_pub:
|
||||
alive = pub is not None and pub.poll() is None
|
||||
log.info("STATUS: publisher %s", "UP" if alive else "DOWN")
|
||||
_shutdown()
|
||||
return 0 if alive else 1
|
||||
up = [i for i, p in sup.procs.items() if p.poll() is None]
|
||||
log.info("STATUS: %d/%d workers alive", len(up), len(sup.procs))
|
||||
_shutdown()
|
||||
return 0 if len(up) == len(sup.procs) else 1
|
||||
|
||||
try:
|
||||
while True:
|
||||
if use_pub:
|
||||
_pump_pub()
|
||||
else:
|
||||
sup.pump()
|
||||
sup.reap()
|
||||
time.sleep(0.2)
|
||||
except KeyboardInterrupt:
|
||||
log.info("shutting down ...")
|
||||
_shutdown()
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user