From dac080acc403812e23a455497b8ae9f3a2ff8506 Mon Sep 17 00:00:00 2001 From: Luc Busquin <133058544+Cybis320@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:54:11 -0700 Subject: [PATCH 1/5] Add configurable GStreamer queue size (gst_queue_size) --- .config | 6 ++++++ RMS/BufferedCapture.py | 9 +++++---- RMS/ConfigReader.py | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.config b/.config index 35c5d4677..d111617f1 100644 --- a/.config +++ b/.config @@ -120,6 +120,12 @@ gst_colorspace: BGR ; Examples: decodebin, avdec_h264, nvh264dec gst_decoder: avdec_h264 +; Max decoded frames buffered per GStreamer queue element. Higher values absorb +; processing spikes without dropping frames. On memory-constrained systems this +; can trigger a swap death spiral. Reduce if experiencing OOM even after reducing +; num_cores to 1. +gst_queue_size: 100 + ; Path to the camera settings json ; e.g. ~/username/source/Stations/XX0001/camera_settings.json ; or ./camera_settings.json diff --git a/RMS/BufferedCapture.py b/RMS/BufferedCapture.py index f0addb2d1..e0ad7ddf1 100644 --- a/RMS/BufferedCapture.py +++ b/RMS/BufferedCapture.py @@ -1097,13 +1097,14 @@ def createGstreamDevice(self, video_format, gst_decoder='decodebin', ).format(protocol_str, device_url) # Branch for processing + queue_size = self.config.gst_queue_size processing_branch = ( "t. ! queue ! {:s} ! " - "queue leaky=downstream max-size-buffers=100 max-size-bytes=0 max-size-time=0 ! " + "queue leaky=downstream max-size-buffers={:d} max-size-bytes=0 max-size-time=0 ! " "videoconvert ! video/x-raw,format={:s} ! " - "queue max-size-buffers=100 max-size-bytes=0 max-size-time=0 ! " - "appsink max-buffers=100 drop=true sync=0 name=appsink" - ).format(gst_decoder, video_format) + "queue max-size-buffers={:d} max-size-bytes=0 max-size-time=0 ! " + "appsink max-buffers={:d} drop=true sync=0 name=appsink" + ).format(gst_decoder, queue_size, video_format, queue_size, queue_size) # Branch for storage - if video_file_dir is not None, save the raw stream to a file if video_file_dir is not None: diff --git a/RMS/ConfigReader.py b/RMS/ConfigReader.py index 0d7de609f..cb56cd430 100644 --- a/RMS/ConfigReader.py +++ b/RMS/ConfigReader.py @@ -298,6 +298,9 @@ def __init__(self): # Decoder for the gstreamer media backend (e.g. decodebin, avdec_h264, nvh264dec) self.gst_decoder = "avdec_h264" + # Max buffers per GStreamer queue element (lower values reduce memory usage on multi-cam systems) + self.gst_queue_size = 100 + # Path to the json file containing camera settings self.camera_settings_path = "./camera_settings.json" @@ -1161,6 +1164,9 @@ def parseCapture(config, parser): if parser.has_option(section, "gst_decoder"): config.gst_decoder = parser.get(section, "gst_decoder") + if parser.has_option(section, "gst_queue_size"): + config.gst_queue_size = parser.getint(section, "gst_queue_size") + if parser.has_option(section, "camera_settings_path") and os.path.isfile(parser.get(section, "camera_settings_path")): config.camera_settings_path = parser.get(section, "camera_settings_path") else: From 2c94e57d5cb7d6d019269d13bc4d082399956b04 Mon Sep 17 00:00:00 2001 From: Cybis320 Date: Wed, 18 Mar 2026 23:37:40 +0000 Subject: [PATCH 2/5] Fix memory leaks: zombie Compressor processes and inherited shared buffers --- RMS/BufferedCapture.py | 50 ++++++++++++++++++++++++++++++++++++ RMS/Compression.py | 16 ++++++++++-- RMS/DetectStarsAndMeteors.py | 1 + RMS/ExtractStars.py | 1 + RMS/QueuedPool.py | 23 ++++++++++++----- RMS/StartCapture.py | 3 +++ 6 files changed, 86 insertions(+), 8 deletions(-) diff --git a/RMS/BufferedCapture.py b/RMS/BufferedCapture.py index e0ad7ddf1..becb4ee7f 100644 --- a/RMS/BufferedCapture.py +++ b/RMS/BufferedCapture.py @@ -1773,6 +1773,37 @@ def run(self): log.debug("Process-specific initialization complete") + # Prevent GStreamer and other grandchild processes from inheriting the + # large shared frame buffers (~506 MB each at 1080p). These buffers are + # only needed by BufferedCapture and Compressor; any subprocess forked + # from here (e.g. GStreamer NVENC encoder, RawFrameSaver) does not need + # them. The munmap only runs in the CHILD after fork, not here. + try: + _libc = ctypes.CDLL('libc.so.6', use_errno=True) + _libc.munmap.restype = ctypes.c_int + _libc.munmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + + bufsToUnmap = [] + for arr in (self.array1, self.array2): + try: + bufsToUnmap.append((arr.ctypes.data, arr.nbytes)) + except Exception: + pass + + if bufsToUnmap: + def unmapFrameBuffersInChild(): + for addr, sz in bufsToUnmap: + _libc.munmap(addr, sz) + + os.register_at_fork(after_in_child=unmapFrameBuffersInChild) + log.debug("Registered at_fork handler to unmap {:d} frame buffers " + "({:.0f} MB) in grandchild processes".format( + len(bufsToUnmap), + sum(s for _, s in bufsToUnmap) / (1024*1024))) + + except Exception as e: + log.warning("Could not register at_fork handler for frame buffer cleanup: {}".format(e)) + # Main capture loop while not self.exit.is_set() and not self.initVideoDevice(): # Update heartbeat during connection attempts to show we're still alive @@ -2186,6 +2217,25 @@ def captureFrames(self): # Force device re-initialization by releasing and reconnecting log.info("Releasing resources to re-initialize video device with GStreamer") self.releaseResources() + + # If transitioning to daytime, release the compression frame + # buffers back to the OS. They are not used during the day + # (frame writes are skipped in daytime mode) and would otherwise + # sit in swap until nightfall. MADV_DONTNEED drops the pages + # immediately; they get zero-filled on next write at no cost. + if current_daytime: + try: + _libc = ctypes.CDLL('libc.so.6', use_errno=True) + _libc.madvise.restype = ctypes.c_int + _libc.madvise.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] + MADV_DONTNEED = 4 + for arr in (self.array1, self.array2): + _libc.madvise(arr.ctypes.data, arr.nbytes, MADV_DONTNEED) + log.info("Released compression frame buffers ({:.0f} MB) back to OS".format( + sum(a.nbytes for a in (self.array1, self.array2)) / (1024*1024))) + except Exception as e: + log.warning("Could not release frame buffers: {}".format(e)) + wait_for_reconnect = True break diff --git a/RMS/Compression.py b/RMS/Compression.py index 735e62827..7103ba3f5 100644 --- a/RMS/Compression.py +++ b/RMS/Compression.py @@ -238,6 +238,11 @@ def stop(self): # Always join to reap zombie (returns instantly if already dead) self.join() + else: + # Process is not alive but may not have been joined yet - reap it + log.debug("Compression process not alive, joining to reap resources") + self.join(timeout=5) + # Return the detector and live viewer objects because they were updated in this namespace return self.detector @@ -283,9 +288,9 @@ def run(self): if self.exit.is_set(): log.debug('Compression run exit') - self.run_exited.set() - return None + self.run_exited.set() + os._exit(0) time.sleep(0.1) @@ -390,7 +395,14 @@ def run(self): log.debug('Compression run exit') + time.sleep(1.0) self.run_exited.set() + # Force-exit the process. The forked QueuedPool Manager proxy threads + # hold open socket connections that survive even after dropping all + # Python references. os._exit() is the only reliable way to terminate + # the process without waiting for those threads. + os._exit(0) + diff --git a/RMS/DetectStarsAndMeteors.py b/RMS/DetectStarsAndMeteors.py index 54c43a8fd..327937e71 100644 --- a/RMS/DetectStarsAndMeteors.py +++ b/RMS/DetectStarsAndMeteors.py @@ -442,6 +442,7 @@ def detectStarsAndMeteorsDirectory(dir_path, config, output_suffix=''): # Get the detection results from the queue detection_results = detector.getResults() + detector.shutdownManager() # Save detection to disk diff --git a/RMS/ExtractStars.py b/RMS/ExtractStars.py index c9156c906..61b4acccc 100644 --- a/RMS/ExtractStars.py +++ b/RMS/ExtractStars.py @@ -874,6 +874,7 @@ def extractStarsAndSave(config, ff_dir): workpool.closePool() results = workpool.getResults() + workpool.shutdownManager() # Get extraction results diff --git a/RMS/QueuedPool.py b/RMS/QueuedPool.py index b5214c219..2764590c9 100755 --- a/RMS/QueuedPool.py +++ b/RMS/QueuedPool.py @@ -156,17 +156,17 @@ def __init__(self, func, cores=None, log=None, delay_start=0, worker_timeout=200 self.func_kwargs = func_kwargs self.worker_wait_inbetween_jobs = worker_wait_inbetween_jobs - # Initialize queues (for some reason queues from Manager need to be created, otherwise they are + # Initialize queues (for some reason queues from Manager need to be created, otherwise they are # blocking when using get_nowait) - manager = multiprocessing.Manager() + self.manager = multiprocessing.Manager() # Only init with maxsize if given, otherwise it return a TypeError when fed data from Compressor if input_queue_maxsize is None: - self.input_queue = manager.Queue() + self.input_queue = self.manager.Queue() else: - self.input_queue = manager.Queue(maxsize=input_queue_maxsize) + self.input_queue = self.manager.Queue(maxsize=input_queue_maxsize) - self.output_queue = manager.Queue() + self.output_queue = self.manager.Queue() self.func = func self.pool = None @@ -463,7 +463,7 @@ def closePool(self): break - + # If all workers are idle, set the last idle time if all_workers_idle_time is None: @@ -544,6 +544,17 @@ def closePool(self): + def shutdownManager(self): + """ Shut down the Manager server process. Call this after all results have been collected. """ + + if hasattr(self, 'manager') and self.manager is not None: + try: + self.manager.shutdown() + except Exception: + pass + self.manager = None + + def updateCoreNumber(self, cores=None): """ Update the number of cores/workers used by the pool. diff --git a/RMS/StartCapture.py b/RMS/StartCapture.py index e2f02981f..422e73bb3 100644 --- a/RMS/StartCapture.py +++ b/RMS/StartCapture.py @@ -827,6 +827,9 @@ def runCapture(config, duration=None, video_file=None, nodetect=False, detect_en # Get the detection results from the queue detection_results = detector.getResults() + # Shut down the Manager server process now that results are collected + detector.shutdownManager() + else: detection_results = [] From da88039ba8cfd01ea55f801b9cef2d4b500568ba Mon Sep 17 00:00:00 2001 From: Cybis320 Date: Sun, 22 Mar 2026 00:40:22 +0000 Subject: [PATCH 3/5] Add --reboot and --reboot-if-needed flags to GRMSUpdater.sh --- Scripts/MultiCamLinux/GRMSUpdater.sh | 211 +++++++++++++++++---------- 1 file changed, 131 insertions(+), 80 deletions(-) diff --git a/Scripts/MultiCamLinux/GRMSUpdater.sh b/Scripts/MultiCamLinux/GRMSUpdater.sh index 8d6ccc239..cb4f44f95 100755 --- a/Scripts/MultiCamLinux/GRMSUpdater.sh +++ b/Scripts/MultiCamLinux/GRMSUpdater.sh @@ -89,6 +89,93 @@ regex_for() { echo '^/bin/bash[[:space:]]+.*/StartCapture\.sh[[:space:]]+'"$1"'([[:space:]]|$)' } +# ------------------------------------------------------------ +# should_reboot() – check if system reboot is requested +# ------------------------------------------------------------ +should_reboot() { + if [[ "$REBOOT_MODE" == "always" ]]; then + return 0 + elif [[ "$REBOOT_MODE" == "if-needed" && -f /var/run/reboot-required ]]; then + return 0 + fi + return 1 +} + +# ------------------------------------------------------------ +# stop_stations() – gracefully stop all running RMS stations +# ------------------------------------------------------------ +stop_stations() { + if [[ ${#RunList[@]} -gt 0 ]]; then + log_message "Gracefully stopping ${#RunList[@]} running RMS stations: ${RunList[*]}" + + # First, try graceful shutdown with SIGTERM for each station (StartCapture forwards as SIGINT) + for station in "${RunList[@]}"; do + log_message "Sending SIGTERM to all processes for station $station..." + pattern=$(regex_for "$station") + if pkill -f -TERM -- "$pattern" 2>/dev/null; then + log_message "Sent SIGTERM to station $station processes" + else + log_message "Warning: No processes found for station $station (may have already exited)" + fi + done + + # Wait for processes to shut down gracefully (with reasonable timeout) + SHUTDOWN_TIMEOUT=600 # 10 minutes - adjust based on your typical shutdown time + WAIT_INTERVAL=5 + elapsed=0 + + log_message "Waiting up to ${SHUTDOWN_TIMEOUT} seconds for graceful shutdown..." + + while [[ $elapsed -lt $SHUTDOWN_TIMEOUT ]]; do + # Check if any station processes are still running + still_running=() + for station in "${RunList[@]}"; do + pattern=$(regex_for "$station") + if pgrep -f -- "$pattern" >/dev/null 2>&1; then + still_running+=("$station") + fi + done + + if [[ ${#still_running[@]} -eq 0 ]]; then + log_message "All station processes shut down gracefully after ${elapsed} seconds" + break + fi + + log_message "Still waiting for ${#still_running[@]} stations to shutdown: ${still_running[*]} (${elapsed}s elapsed)" + sleep $WAIT_INTERVAL + elapsed=$((elapsed + WAIT_INTERVAL)) + done + + # Force kill any remaining processes if timeout reached + final_check=() + for station in "${RunList[@]}"; do + pattern=$(regex_for "$station") + if pgrep -f -- "$pattern" >/dev/null 2>&1; then + final_check+=("$station") + fi + done + + if [[ ${#final_check[@]} -gt 0 ]]; then + log_message "Timeout reached. Force killing ${#final_check[@]} remaining stations: ${final_check[*]}" + for station in "${final_check[@]}"; do + log_message "Force killing all processes for station $station..." + pattern=$(regex_for "$station") + if pkill -f -KILL -- "$pattern" 2>/dev/null; then + log_message "Force killed station $station processes" + else + log_message "Warning: Could not kill processes for station $station (may have already exited)" + fi + done + + # Give a moment for force kills to take effect + sleep 2 + fi + + else + log_message "No running RMS stations found" + fi +} + # Log script start log_message "GRMSUpdater.sh started with args: $*" @@ -103,6 +190,7 @@ fi # Parse command line arguments FORCE_UPDATE=false PREFERRED_TERM="lxterminal" # default terminal +REBOOT_MODE="none" POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do @@ -115,6 +203,14 @@ while [[ $# -gt 0 ]]; do FORCE_UPDATE=true shift ;; + --reboot-if-needed) + REBOOT_MODE="if-needed" + shift + ;; + --reboot) + REBOOT_MODE="always" + shift + ;; *) POSITIONAL_ARGS+=("$1") shift @@ -181,6 +277,16 @@ else log_message "No DISPLAY available - GUI terminals will fail, consider using --term tmux" fi +# Find running stations by looking for StartCapture.sh processes +mapfile -t RunList < <( + pgrep -f "Scripts/MultiCamLinux/StartCapture.sh" | while read -r pid; do + cmdline=$(ps -p "$pid" -o args= 2>/dev/null || continue) + if [[ "$cmdline" =~ Scripts/MultiCamLinux/StartCapture\.sh[[:space:]]+([[:alnum:]]{6}) ]]; then + echo "${BASH_REMATCH[1]}" + fi + done | sort -u +) + # Check if updates are actually needed before disrupting running processes (unless --force is used) if [[ "$FORCE_UPDATE" != "true" ]]; then cd "$RMS_DIR" || { log_message "Error: RMS directory not found at $RMS_DIR"; exit 1; } @@ -195,6 +301,18 @@ if [[ "$FORCE_UPDATE" != "true" ]]; then MODIFIED_FILES=$(git diff --name-only | grep -v -E '^(\.config|camera_settings\.json)$' || true) if [[ -n "$REMOTE_SHA" && "$REMOTE_SHA" == "$LOCAL_SHA" && -z "$MODIFIED_FILES" ]]; then + if should_reboot; then + if sudo -n shutdown --help >/dev/null 2>&1; then + log_message "No RMS update needed, but system reboot is required" + stop_stations + log_message "Rebooting system..." + sudo shutdown -r now + exit 0 + else + log_message "WARNING: System reboot needed but sudo shutdown not available - skipping reboot" + log_message "Configure /etc/sudoers.d/rms-reboot to enable passwordless shutdown" + fi + fi log_message "RMS is already up to date ($CURRENT_BRANCH: $LOCAL_SHA) and no tracked file modifications - no need to restart stations" log_message "Use --force to restart stations anyway" log_message "GRMSUpdater.sh completed successfully (early exit - no updates needed)" @@ -214,86 +332,8 @@ else cd "$RMS_DIR" || { log_message "Error: RMS directory not found at $RMS_DIR"; exit 1; } fi -# Find running stations by looking for StartCapture.sh processes -mapfile -t RunList < <( - pgrep -f "Scripts/MultiCamLinux/StartCapture.sh" | while read -r pid; do - cmdline=$(ps -p "$pid" -o args= 2>/dev/null || continue) - if [[ "$cmdline" =~ Scripts/MultiCamLinux/StartCapture\.sh[[:space:]]+([[:alnum:]]{6}) ]]; then - echo "${BASH_REMATCH[1]}" - fi - done | sort -u -) - -# Only stop processes if we actually need to update -if [[ ${#RunList[@]} -gt 0 ]]; then - log_message "Gracefully stopping ${#RunList[@]} running RMS stations for update: ${RunList[*]}" - - # First, try graceful shutdown with SIGTERM for each station (StartCapture forwards as SIGINT) - for station in "${RunList[@]}"; do - log_message "Sending SIGTERM to all processes for station $station..." - pattern=$(regex_for "$station") - if pkill -f -TERM -- "$pattern" 2>/dev/null; then - log_message "Sent SIGTERM to station $station processes" - else - log_message "Warning: No processes found for station $station (may have already exited)" - fi - done - - # Wait for processes to shut down gracefully (with reasonable timeout) - SHUTDOWN_TIMEOUT=600 # 10 minutes - adjust based on your typical shutdown time - WAIT_INTERVAL=5 - elapsed=0 - - log_message "Waiting up to ${SHUTDOWN_TIMEOUT} seconds for graceful shutdown..." - - while [[ $elapsed -lt $SHUTDOWN_TIMEOUT ]]; do - # Check if any station processes are still running - still_running=() - for station in "${RunList[@]}"; do - pattern=$(regex_for "$station") - if pgrep -f -- "$pattern" >/dev/null 2>&1; then - still_running+=("$station") - fi - done - - if [[ ${#still_running[@]} -eq 0 ]]; then - log_message "All station processes shut down gracefully after ${elapsed} seconds" - break - fi - - log_message "Still waiting for ${#still_running[@]} stations to shutdown: ${still_running[*]} (${elapsed}s elapsed)" - sleep $WAIT_INTERVAL - elapsed=$((elapsed + WAIT_INTERVAL)) - done - - # Force kill any remaining processes if timeout reached - final_check=() - for station in "${RunList[@]}"; do - pattern=$(regex_for "$station") - if pgrep -f -- "$pattern" >/dev/null 2>&1; then - final_check+=("$station") - fi - done - - if [[ ${#final_check[@]} -gt 0 ]]; then - log_message "Timeout reached. Force killing ${#final_check[@]} remaining stations: ${final_check[*]}" - for station in "${final_check[@]}"; do - log_message "Force killing all processes for station $station..." - pattern=$(regex_for "$station") - if pkill -f -KILL -- "$pattern" 2>/dev/null; then - log_message "Force killed station $station processes" - else - log_message "Warning: Could not kill processes for station $station (may have already exited)" - fi - done - - # Give a moment for force kills to take effect - sleep 2 - fi - -else - log_message "No running RMS stations found" -fi +# Stop running stations before update +stop_stations # Note: When run from user cron, DISPLAY may not be set. Terminal launching will fall back to tmux if needed. @@ -411,6 +451,17 @@ else log_message "Warning: RMS update failed, but continuing to restart stations since they were already stopped" fi +# Check if system reboot is needed after update +if should_reboot; then + log_message "Attempting system reboot after RMS update..." + if sudo -n shutdown -r now 2>/dev/null; then + exit 0 + else + log_message "WARNING: Reboot failed (sudo shutdown not available) - restarting stations instead" + log_message "Configure /etc/sudoers.d/rms-reboot to enable passwordless shutdown" + fi +fi + if [[ ${#POSITIONAL_ARGS[@]} -eq 0 ]]; then # Called with no args - restart all configured stations log_message "Will restart all configured stations post-update" From f757025609a50ea4633192b929d9e69dde0d431f Mon Sep 17 00:00:00 2001 From: Cybis320 Date: Sun, 22 Mar 2026 18:21:27 +0000 Subject: [PATCH 4/5] Add kernel mismatch detection to should_reboot() for RPi/Debian support --- Scripts/MultiCamLinux/GRMSUpdater.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Scripts/MultiCamLinux/GRMSUpdater.sh b/Scripts/MultiCamLinux/GRMSUpdater.sh index cb4f44f95..fd0c4d85b 100755 --- a/Scripts/MultiCamLinux/GRMSUpdater.sh +++ b/Scripts/MultiCamLinux/GRMSUpdater.sh @@ -95,8 +95,19 @@ regex_for() { should_reboot() { if [[ "$REBOOT_MODE" == "always" ]]; then return 0 - elif [[ "$REBOOT_MODE" == "if-needed" && -f /var/run/reboot-required ]]; then - return 0 + elif [[ "$REBOOT_MODE" == "if-needed" ]]; then + # Ubuntu/Debian: flag file created by update-notifier-common or linux-base ≥4.13 + if [[ -f /var/run/reboot-required ]]; then + return 0 + fi + # Fallback: compare running kernel to latest installed (works on RPi OS / Debian) + local running latest + running="$(uname -r)" + latest="$(ls /lib/modules/ | sort -V | tail -1)" + if [[ -n "$latest" && "$running" != "$latest" ]]; then + log_message "Kernel mismatch: running $running, installed $latest" + return 0 + fi fi return 1 } From 40ec9fff7c30fe7f9256b55c838819471026bd70 Mon Sep 17 00:00:00 2001 From: Cybis320 Date: Wed, 25 Mar 2026 06:51:32 +0000 Subject: [PATCH 5/5] Fix segfault on Python 3.8: replace at_fork munmap with MADV_DONTFORK --- RMS/BufferedCapture.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/RMS/BufferedCapture.py b/RMS/BufferedCapture.py index becb4ee7f..037b1481d 100644 --- a/RMS/BufferedCapture.py +++ b/RMS/BufferedCapture.py @@ -1777,32 +1777,31 @@ def run(self): # large shared frame buffers (~506 MB each at 1080p). These buffers are # only needed by BufferedCapture and Compressor; any subprocess forked # from here (e.g. GStreamer NVENC encoder, RawFrameSaver) does not need - # them. The munmap only runs in the CHILD after fork, not here. + # them. MADV_DONTFORK tells the kernel to exclude these pages from any + # child created via fork(), so grandchildren never see them at all. + # Compressor is unaffected because it forks from StartCapture, not here. try: _libc = ctypes.CDLL('libc.so.6', use_errno=True) - _libc.munmap.restype = ctypes.c_int - _libc.munmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + _libc.madvise.restype = ctypes.c_int + _libc.madvise.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] + MADV_DONTFORK = 10 - bufsToUnmap = [] + total_bytes = 0 for arr in (self.array1, self.array2): try: - bufsToUnmap.append((arr.ctypes.data, arr.nbytes)) + ret = _libc.madvise(arr.ctypes.data, arr.nbytes, MADV_DONTFORK) + if ret == 0: + total_bytes += arr.nbytes except Exception: pass - if bufsToUnmap: - def unmapFrameBuffersInChild(): - for addr, sz in bufsToUnmap: - _libc.munmap(addr, sz) - - os.register_at_fork(after_in_child=unmapFrameBuffersInChild) - log.debug("Registered at_fork handler to unmap {:d} frame buffers " - "({:.0f} MB) in grandchild processes".format( - len(bufsToUnmap), - sum(s for _, s in bufsToUnmap) / (1024*1024))) + if total_bytes: + log.debug("Marked frame buffers ({:.0f} MB) as DONTFORK to prevent " + "inheritance by grandchild processes".format( + total_bytes / (1024*1024))) except Exception as e: - log.warning("Could not register at_fork handler for frame buffer cleanup: {}".format(e)) + log.warning("Could not set MADV_DONTFORK on frame buffers: {}".format(e)) # Main capture loop while not self.exit.is_set() and not self.initVideoDevice():