Bläddra i källkod

feat(gui): 添加暂停/恢复功能并优化倒计时显示

- 在 GUI 中添加暂停/恢复按钮,支持手动控制 protector 运行状态
- 优化倒计时逻辑,正确显示 protector 的下次运行时间
- 修复 protector 初始延迟逻辑,避免启动时立即执行
- 实现 ProtectorRunner 的 pause/resume 控制接口
- 改进后台循环逻辑,支持响应式暂停与恢复操作
- 添加 get_next_run_time 方法用于获取下次计划运行时间戳
mcbaiyun 2 månader sedan
förälder
incheckning
38be3cc68d
3 ändrade filer med 134 tillägg och 11 borttagningar
  1. 75 7
      gui.py
  2. 3 3
      main.py
  3. 56 1
      protector.py

+ 75 - 7
gui.py

@@ -9,6 +9,7 @@ from __future__ import annotations
 
 import json
 import threading
+import time
 import tkinter as tk
 from tkinter import messagebox
 from pathlib import Path
@@ -114,6 +115,12 @@ class ProtectorGUI(tk.Tk):
         self.countdown_label = tk.Label(self, textvariable=self.countdown_var)
         self.countdown_label.pack(anchor="ne", padx=8, pady=4)
 
+        # Pause/Resume button
+        self.pause_btn = None
+        if self.protector_runner is not None:
+            self.pause_btn = tk.Button(self, text="Pause", command=self._toggle_pause)
+            self.pause_btn.pack(anchor="ne", padx=8)
+
         # start initial delayed run countdown (10 seconds) if runner provided
         if self.protector_runner is not None:
             self._initial_delay = 10
@@ -247,24 +254,70 @@ class ProtectorGUI(tk.Tk):
             if not self.protector_runner:
                 return
             # Update label
+            # compute seconds until next run based on protector_runner's schedule
             try:
-                self.countdown_var.set(f"下一次检测:{self._countdown_seconds}s")
+                if self.protector_runner.is_paused():
+                    self.countdown_var.set("Protector: paused")
+                else:
+                    next_ts = self.protector_runner.get_next_run_time()
+                    if next_ts is None:
+                        # fallback to internal counter
+                        secs = max(0, int(self._countdown_seconds))
+                    else:
+                        secs = max(0, int(round(next_ts - time.time())))
+                    self.countdown_var.set(f"下一次检测:{secs}s")
             except Exception:
                 self.countdown_var.set("Protector: running")
 
-            if self._countdown_seconds <= 0:
-                # trigger a run in background
+            # if protector hasn't been started yet, use internal countdown
+            try:
+                started = getattr(self.protector_runner, "_started", False)
+            except Exception:
+                started = False
+
+            if not started:
+                # use internal countdown to start
+                if self._countdown_seconds <= 0:
+                    do_start = True
+                else:
+                    do_start = False
+            else:
+                # protector already started; rely on its internal schedule
+                do_start = False
+
+            if do_start:
+                # If protector runner not started yet, start its loop first
                 try:
-                    threading.Thread(target=self.protector_runner.run_once, daemon=True).start()
-                except Exception as e:
-                    print("触发 protector 运行失败:", e)
+                    if not getattr(self.protector_runner, "_started", False):
+                        # start the background loop
+                        self.protector_runner._started = True
+                        threading.Thread(target=self.protector_runner.start, daemon=True).start()
+                except Exception:
+                    pass
+
+                # ProtectorRunner.start() will perform an initial run, so do not
+                # explicitly call run_once() here to avoid double execution.
+                # reset internal countdown to interval after start
+                try:
+                    self._countdown_seconds = int(self.protector_runner.get_interval())
+                except Exception:
+                    self._countdown_seconds = self._initial_delay
                 # reset countdown to configured interval
                 try:
                     self._countdown_seconds = int(self.protector_runner.get_interval())
                 except Exception:
                     self._countdown_seconds = self._initial_delay
+                # update pause button text
+                try:
+                    if self.pause_btn:
+                        self.pause_btn.config(text=("Resume" if self.protector_runner.is_paused() else "Pause"))
+                except Exception:
+                    pass
             else:
-                self._countdown_seconds -= 1
+                # when protector already started, we won't decrement internal counter
+                # keep internal counter but keep ticking for display purposes
+                if not started:
+                    self._countdown_seconds -= 1
 
         except Exception as e:
             # keep ticking even on error
@@ -276,6 +329,21 @@ class ProtectorGUI(tk.Tk):
         except Exception:
             pass
 
+    def _toggle_pause(self):
+        if not self.protector_runner:
+            return
+        try:
+            if self.protector_runner.is_paused():
+                self.protector_runner.resume()
+                if self.pause_btn:
+                    self.pause_btn.config(text="Pause")
+            else:
+                self.protector_runner.pause()
+                if self.pause_btn:
+                    self.pause_btn.config(text="Resume")
+        except Exception as e:
+            messagebox.showerror("错误", f"切换暂停/恢复失败: {e}")
+
 def run_gui(protector_runner=None):
     app = ProtectorGUI(protector_runner=protector_runner)
     app.mainloop()

+ 3 - 3
main.py

@@ -17,10 +17,10 @@ def main():
 
 	# If --gui passed, launch GUI and start protector runner
 	if "--gui" in sys.argv:
-		# start background protector
+		# create protector runner but DO NOT start it yet.
+		# GUI will start the protector after initial countdown to avoid immediate runs.
 		pr = ProtectorRunner()
-		pr.start()
-		# launch GUI (blocking) and pass protector runner so GUI can display countdown
+		# launch GUI (blocking) and pass protector runner so GUI can control its start
 		gui.run_gui(protector_runner=pr)
 		# when GUI exits, stop protector
 		pr.stop()

+ 56 - 1
protector.py

@@ -21,8 +21,19 @@ class ProtectorRunner:
         self.interval = int(cfg.get("scan_interval", 60))
         self._stop = threading.Event()
         self.last_run_time: Optional[float] = None
+        # pause control: _pause_event is set when running, cleared when paused
+        self._pause_event = threading.Event()
+        self._pause_event.set()
 
     def run_once(self):
+        # if paused, skip immediate execution (protect against external triggers)
+        try:
+            if not self._pause_event.is_set():
+                logger.info("Protector is paused, skipping run_once")
+                return
+        except Exception:
+            pass
+
         # record start time to help external coordinators avoid duplicate runs
         try:
             self.last_run_time = time.time()
@@ -70,6 +81,13 @@ class ProtectorRunner:
         self._stop.clear()
         def _loop():
             while not self._stop.is_set():
+                # respect pause state
+                if not self._pause_event.is_set():
+                    # paused: wait until unpaused or stopped
+                    # wake every 1s to check stop flag
+                    self._pause_event.wait(timeout=1)
+                    continue
+
                 try:
                     self.run_once()
                 except Exception:
@@ -80,7 +98,14 @@ class ProtectorRunner:
                     self.interval = int(cfg.get("scan_interval", self.interval))
                 except Exception:
                     pass
-                time.sleep(self.interval)
+
+                # sleep in one-second steps so we can be responsive to pause/stop
+                slept = 0
+                while slept < self.interval and not self._stop.is_set():
+                    if not self._pause_event.is_set():
+                        break
+                    time.sleep(1)
+                    slept += 1
 
         t = threading.Thread(target=_loop, daemon=True)
         t.start()
@@ -98,6 +123,36 @@ class ProtectorRunner:
         except Exception:
             return self.interval
 
+    def get_next_run_time(self) -> float | None:
+        """Return the epoch timestamp of the next scheduled run, or None if unknown."""
+        try:
+            interval = self.get_interval()
+            if self.last_run_time:
+                return float(self.last_run_time + interval)
+            # if never run, next run is interval seconds from now (if started)
+            return float(time.time() + interval)
+        except Exception:
+            return None
+
+    def pause(self):
+        """Pause periodic execution."""
+        try:
+            self._pause_event.clear()
+            logger.info("Protector 已暂停")
+        except Exception:
+            pass
+
+    def resume(self):
+        """Resume periodic execution."""
+        try:
+            self._pause_event.set()
+            logger.info("Protector 已恢复")
+        except Exception:
+            pass
+
+    def is_paused(self) -> bool:
+        return not self._pause_event.is_set()
+
 
 def run_protector_blocking_once():
     pr = ProtectorRunner()