Browse Source

fix(touch): resolve 100% CPU on screen timeout with proper thread cleanup

Root cause: Touch monitoring thread had blocking readline() that couldn't
exit when screen woke up, plus missing cleanup in _turn_screen_on().

Changes:
- Add threading.Event stop flag for clean thread signaling
- Use select() with 0.5s timeout for non-blocking reads
- Add thread cleanup in _turn_screen_on() with join timeout
- Add finally block for guaranteed subprocess termination
- Check stop flag during all delays and loops

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 tuần trước cách đây
mục cha
commit
1881d1006f
1 tập tin đã thay đổi với 121 bổ sung79 xóa
  1. 121 79
      dune-weaver-touch/backend.py

+ 121 - 79
dune-weaver-touch/backend.py

@@ -134,6 +134,7 @@ class Backend(QObject):
         self._screen_timeout = self.DEFAULT_SCREEN_TIMEOUT  # Will be loaded from settings
         self._last_activity = time.time()
         self._touch_monitor_thread = None
+        self._stop_touch_monitor = threading.Event()  # Signal to stop touch monitoring thread
         self._screen_transition_lock = threading.Lock()  # Prevent rapid state changes
         self._last_screen_change = 0  # Track last state change time
         self._use_touch_script = False  # Disable external touch-monitor script (too sensitive)
@@ -1070,7 +1071,17 @@ class Backend(QObject):
                 self._screen_on = True
                 self._last_screen_change = time.time()
                 self.screenStateChanged.emit(True)
-            
+
+                # Signal touch monitor thread to stop
+                self._stop_touch_monitor.set()
+
+                # Give thread time to exit gracefully
+                if self._touch_monitor_thread and self._touch_monitor_thread.is_alive():
+                    print("🖥️ Waiting for touch monitor thread to stop...")
+                    self._touch_monitor_thread.join(timeout=1.0)
+                    if self._touch_monitor_thread.is_alive():
+                        print("⚠️ Touch monitor thread still running (subprocess will be killed)")
+
             except Exception as e:
                 print(f"❌ Failed to turn screen on: {e}")
     
@@ -1153,65 +1164,83 @@ class Backend(QObject):
     def _start_touch_monitoring(self):
         """Start monitoring touch input for wake-up"""
         if self._touch_monitor_thread is None or not self._touch_monitor_thread.is_alive():
+            # Reset stop flag before starting new thread
+            self._stop_touch_monitor.clear()
             self._touch_monitor_thread = threading.Thread(target=self._monitor_touch_input, daemon=True)
             self._touch_monitor_thread.start()
     
     def _monitor_touch_input(self):
         """Monitor touch input to wake up the screen"""
+        import select
+
         print("👆 Starting touch monitoring for wake-up")
-        # Add delay to let any residual touch events clear
-        time.sleep(2)
-        
-        # Flush touch device to clear any buffered events
-        try:
-            # Find and flush touch device
-            for i in range(5):
-                device = f'/dev/input/event{i}'
-                if Path(device).exists():
-                    try:
-                        # Read and discard any pending events
-                        with open(device, 'rb') as f:
-                            import fcntl
-                            import os
-                            fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
-                            while True:
-                                try:
-                                    f.read(24)  # Standard input_event size
-                                except:
-                                    break
-                        print(f"👆 Flushed touch device: {device}")
-                        break
-                    except:
-                        continue
-        except Exception as e:
-            print(f"👆 Could not flush touch device: {e}")
-        
-        print("👆 Touch monitoring active")
+        process = None
+
         try:
+            # Add delay to let any residual touch events clear
+            # Check stop flag during delay to allow quick exit
+            for _ in range(20):  # 2 seconds in 100ms increments
+                if self._stop_touch_monitor.is_set() or self._screen_on:
+                    print("👆 Touch monitoring cancelled during startup delay")
+                    return
+                time.sleep(0.1)
+
+            # Flush touch device to clear any buffered events
+            try:
+                import fcntl
+                # Find and flush touch device
+                for i in range(5):
+                    device = f'/dev/input/event{i}'
+                    if Path(device).exists():
+                        try:
+                            # Read and discard any pending events
+                            with open(device, 'rb') as f:
+                                fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
+                                while True:
+                                    try:
+                                        f.read(24)  # Standard input_event size
+                                    except:
+                                        break
+                            print(f"👆 Flushed touch device: {device}")
+                            break
+                        except:
+                            continue
+            except Exception as e:
+                print(f"👆 Could not flush touch device: {e}")
+
+            # Check again before starting monitoring
+            if self._stop_touch_monitor.is_set() or self._screen_on:
+                print("👆 Touch monitoring cancelled before starting")
+                return
+
+            print("👆 Touch monitoring active")
+
             # Use external touch monitor script if available - but only if not too sensitive
             touch_monitor_script = Path('/usr/local/bin/touch-monitor')
             use_script = touch_monitor_script.exists() and hasattr(self, '_use_touch_script') and self._use_touch_script
-            
+
             if use_script:
                 print("👆 Using touch-monitor script")
                 # Add extra delay for script-based monitoring since it's more sensitive
-                time.sleep(3)
+                for _ in range(30):  # 3 seconds in 100ms increments
+                    if self._stop_touch_monitor.is_set() or self._screen_on:
+                        print("👆 Touch monitoring cancelled during script delay")
+                        return
+                    time.sleep(0.1)
+
                 print("👆 Starting touch-monitor script after flush delay")
-                process = subprocess.Popen(['sudo', '/usr/local/bin/touch-monitor'], 
-                                         stdout=subprocess.PIPE, 
+                process = subprocess.Popen(['sudo', '/usr/local/bin/touch-monitor'],
+                                         stdout=subprocess.PIPE,
                                          stderr=subprocess.PIPE)
-                
+
                 # Wait for script to detect touch and wake screen
-                while not self._screen_on:
+                while not self._screen_on and not self._stop_touch_monitor.is_set():
                     if process.poll() is not None:  # Script exited (touch detected)
                         print("👆 Touch detected by monitor script")
                         self._turn_screen_on()
                         self._reset_activity_timer()
                         break
                     time.sleep(0.1)
-                
-                if process.poll() is None:
-                    process.terminate()
             else:
                 # Fallback: Direct monitoring
                 # Find touch input device
@@ -1221,81 +1250,94 @@ class Backend(QObject):
                     if Path(device).exists():
                         # Check if it's a touch device
                         try:
-                            info = subprocess.run(['udevadm', 'info', '--query=all', f'--name={device}'], 
+                            info = subprocess.run(['udevadm', 'info', '--query=all', f'--name={device}'],
                                                 capture_output=True, text=True, timeout=2)
                             if 'touch' in info.stdout.lower() or 'ft5406' in info.stdout.lower():
                                 touch_device = device
                                 break
                         except:
                             pass
-                
+
                 if not touch_device:
                     touch_device = '/dev/input/event0'  # Default fallback
-                
+
                 print(f"👆 Monitoring touch device: {touch_device}")
-                
+
                 # Try evtest first (more responsive to single taps)
-                evtest_available = subprocess.run(['which', 'evtest'], 
+                evtest_available = subprocess.run(['which', 'evtest'],
                                                  capture_output=True).returncode == 0
-                
+
                 if evtest_available:
                     # Use evtest which is more sensitive to single touches
-                    print("👆 Using evtest for touch detection")
-                    process = subprocess.Popen(['sudo', 'evtest', touch_device], 
-                                             stdout=subprocess.PIPE, 
+                    print("👆 Using evtest for touch detection (non-blocking)")
+                    process = subprocess.Popen(['sudo', 'evtest', touch_device],
+                                             stdout=subprocess.PIPE,
                                              stderr=subprocess.DEVNULL,
                                              text=True)
-                    
-                    # Wait for any event line
-                    while not self._screen_on:
-                        try:
-                            line = process.stdout.readline()
-                            if line and 'Event:' in line:
-                                print("👆 Touch detected via evtest - waking screen")
-                                process.terminate()
-                                self._turn_screen_on()
-                                self._reset_activity_timer()
-                                break
-                        except:
-                            pass
-                        
+
+                    # Wait for any event line using select for non-blocking reads
+                    while not self._screen_on and not self._stop_touch_monitor.is_set():
+                        # Check if process exited
                         if process.poll() is not None:
+                            print("👆 evtest process exited unexpectedly")
+                            break
+
+                        # Use select with timeout for non-blocking read
+                        try:
+                            ready, _, _ = select.select([process.stdout], [], [], 0.5)
+                            if ready:
+                                line = process.stdout.readline()
+                                if line and 'Event:' in line:
+                                    print("👆 Touch detected via evtest - waking screen")
+                                    self._turn_screen_on()
+                                    self._reset_activity_timer()
+                                    break
+                        except Exception as e:
+                            print(f"👆 Error reading evtest output: {e}")
                             break
-                        time.sleep(0.01)  # Small sleep to prevent CPU spinning
                 else:
                     # Fallback: Use cat with single byte read (more responsive)
                     print("👆 Using cat for touch detection")
-                    process = subprocess.Popen(['sudo', 'cat', touch_device], 
-                                             stdout=subprocess.PIPE, 
+                    process = subprocess.Popen(['sudo', 'cat', touch_device],
+                                             stdout=subprocess.PIPE,
                                              stderr=subprocess.DEVNULL)
-                    
+
                     # Wait for any data (even 1 byte indicates touch)
-                    while not self._screen_on:
+                    while not self._screen_on and not self._stop_touch_monitor.is_set():
+                        # Check if process exited
+                        if process.poll() is not None:
+                            print("👆 cat process exited unexpectedly")
+                            break
+
                         try:
-                            # Non-blocking check for data
-                            import select
-                            ready, _, _ = select.select([process.stdout], [], [], 0.1)
+                            # Non-blocking check for data with timeout
+                            ready, _, _ = select.select([process.stdout], [], [], 0.5)
                             if ready:
                                 data = process.stdout.read(1)  # Read just 1 byte
                                 if data:
                                     print("👆 Touch detected - waking screen")
-                                    process.terminate()
                                     self._turn_screen_on()
                                     self._reset_activity_timer()
                                     break
-                        except:
-                            pass
-                        
-                        # Check if screen was turned on by other means
-                        if self._screen_on:
-                            process.terminate()
+                        except Exception as e:
+                            print(f"👆 Error reading touch input: {e}")
                             break
-                        
-                        time.sleep(0.1)
-                
+
         except Exception as e:
             print(f"❌ Error monitoring touch input: {e}")
 
+        finally:
+            # Always clean up subprocess
+            if process is not None and process.poll() is None:
+                print("👆 Terminating touch monitoring subprocess")
+                process.terminate()
+                try:
+                    process.wait(timeout=2)
+                except subprocess.TimeoutExpired:
+                    print("👆 Force killing touch monitoring subprocess")
+                    process.kill()
+                    process.wait()
+
         print("👆 Touch monitoring stopped")
 
     # ==================== LED Control Methods ====================