gui.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. """A simple tkinter GUI to edit config.json and manage protector list.
  2. Features:
  3. - Edit base_url, conn_threshold, scan_interval (saved to config.json)
  4. - Manage protector_list entries (add/edit/remove) stored in protector_list.json
  5. - On saving base_url, attempt login via auth_session.login() and store sess_key
  6. """
  7. from __future__ import annotations
  8. import json
  9. import threading
  10. import time
  11. import tkinter as tk
  12. from tkinter import messagebox
  13. from pathlib import Path
  14. from auth_session import load_config, get_base_url, login, set_sess_key
  15. import protector_list
  16. ROOT = Path(__file__).resolve().parent
  17. CONFIG_PATH = ROOT / "config.json"
  18. def load_cfg():
  19. return load_config()
  20. def save_cfg(cfg: dict):
  21. with open(CONFIG_PATH, "w", encoding="utf-8") as f:
  22. json.dump(cfg, f, ensure_ascii=False, indent=2)
  23. class ProtectorGUI(tk.Tk):
  24. def __init__(self, protector_runner=None):
  25. super().__init__()
  26. self.title("Protector GUI")
  27. self.geometry("700x500")
  28. self.cfg = load_cfg()
  29. # Config frame
  30. cf = tk.LabelFrame(self, text="Config")
  31. cf.pack(fill="x", padx=8, pady=6)
  32. tk.Label(cf, text="base_url:").grid(row=0, column=0, sticky="w")
  33. self.base_entry = tk.Entry(cf, width=60)
  34. self.base_entry.grid(row=0, column=1, padx=4, pady=2)
  35. tk.Label(cf, text="conn_threshold:").grid(row=1, column=0, sticky="w")
  36. self.th_entry = tk.Entry(cf, width=10)
  37. self.th_entry.grid(row=1, column=1, sticky="w", padx=4, pady=2)
  38. tk.Label(cf, text="test_prefix:").grid(row=1, column=2, sticky="w", padx=(12,0))
  39. self.test_prefix_entry = tk.Entry(cf, width=15)
  40. self.test_prefix_entry.grid(row=1, column=3, sticky="w", padx=4, pady=2)
  41. tk.Label(cf, text="rule_ip_limit:").grid(row=2, column=2, sticky="w", padx=(12,0))
  42. self.rule_limit_entry = tk.Entry(cf, width=15)
  43. self.rule_limit_entry.grid(row=2, column=3, sticky="w", padx=4, pady=2)
  44. tk.Label(cf, text="scan_interval (s):").grid(row=2, column=0, sticky="w")
  45. self.interval_entry = tk.Entry(cf, width=10)
  46. self.interval_entry.grid(row=2, column=1, sticky="w", padx=4, pady=2)
  47. tk.Button(cf, text="Save Config", command=self.save_config).grid(row=3, column=1, sticky="w", pady=6)
  48. # Protector list frame
  49. lf = tk.LabelFrame(self, text="Protector List")
  50. lf.pack(fill="both", expand=True, padx=8, pady=6)
  51. self.listbox = tk.Listbox(lf)
  52. self.listbox.pack(side="left", fill="both", expand=True, padx=4, pady=4)
  53. self.listbox.bind("<<ListboxSelect>>", self.on_select)
  54. right = tk.Frame(lf)
  55. right.pack(side="right", fill="y", padx=4)
  56. tk.Label(right, text="target_ip:").pack(anchor="w")
  57. self.ip_entry = tk.Entry(right)
  58. self.ip_entry.pack(fill="x")
  59. tk.Label(right, text="src_port:").pack(anchor="w")
  60. self.port_entry = tk.Entry(right)
  61. self.port_entry.pack(fill="x")
  62. tk.Label(right, text="threshold (optional):").pack(anchor="w")
  63. self.entry_threshold = tk.Entry(right)
  64. self.entry_threshold.pack(fill="x")
  65. tk.Button(right, text="Add", command=self.add_entry).pack(fill="x", pady=4)
  66. tk.Button(right, text="Update", command=self.update_entry).pack(fill="x", pady=4)
  67. tk.Button(right, text="Remove", command=self.remove_entry).pack(fill="x", pady=4)
  68. self.load_values()
  69. # 自动在后台尝试登录(启动后立即)
  70. def _auto_login():
  71. try:
  72. resp, sess_cookie = login()
  73. if sess_cookie:
  74. set_sess_key(sess_cookie)
  75. # 不弹出大量通知,只有在需要时可打开下面这一行
  76. # messagebox.showinfo("登录", f"自动登录完成, 状态: {resp.status_code}")
  77. except Exception as e:
  78. # 登录失败不阻塞 GUI,记录到控制台/弹出一条错误提示
  79. try:
  80. messagebox.showerror("自动登录失败", str(e))
  81. except Exception:
  82. print("自动登录失败:", e)
  83. threading.Thread(target=_auto_login, daemon=True).start()
  84. # Protector runner instance (optional) for countdown and triggering runs
  85. self.protector_runner = protector_runner
  86. # countdown label
  87. self.countdown_var = tk.StringVar(value="Protector: idle")
  88. self.countdown_label = tk.Label(self, textvariable=self.countdown_var)
  89. self.countdown_label.pack(anchor="ne", padx=8, pady=4)
  90. # Pause/Resume button
  91. self.pause_btn = None
  92. if self.protector_runner is not None:
  93. self.pause_btn = tk.Button(self, text="Pause", command=self._toggle_pause)
  94. self.pause_btn.pack(anchor="ne", padx=8)
  95. # start initial delayed run countdown (10 seconds) if runner provided
  96. if self.protector_runner is not None:
  97. self._initial_delay = 10
  98. self._countdown_seconds = self._initial_delay
  99. # schedule countdown update every 1 second via tkinter's after
  100. self.after(1000, self._update_countdown)
  101. def load_values(self):
  102. self.cfg = load_cfg()
  103. self.base_entry.delete(0, tk.END)
  104. self.base_entry.insert(0, self.cfg.get("base_url", ""))
  105. self.th_entry.delete(0, tk.END)
  106. self.th_entry.insert(0, str(self.cfg.get("conn_threshold", "")))
  107. # new fields
  108. self.test_prefix_entry.delete(0, tk.END)
  109. self.test_prefix_entry.insert(0, str(self.cfg.get("test_prefix", "Test_")))
  110. self.rule_limit_entry.delete(0, tk.END)
  111. self.rule_limit_entry.insert(0, str(self.cfg.get("rule_ip_limit", 1000)))
  112. self.interval_entry.delete(0, tk.END)
  113. self.interval_entry.insert(0, str(self.cfg.get("scan_interval", 60)))
  114. self.reload_listbox()
  115. def reload_listbox(self):
  116. self.listbox.delete(0, tk.END)
  117. items = protector_list.load_list()
  118. for i in items:
  119. self.listbox.insert(tk.END, f"{i['id']}: {i['target_ip']}:{i['src_port']} (th={i.get('threshold')})")
  120. def save_config(self):
  121. try:
  122. self.cfg["base_url"] = self.base_entry.get().strip()
  123. self.cfg["conn_threshold"] = int(self.th_entry.get().strip())
  124. # optional new fields
  125. self.cfg["test_prefix"] = str(self.test_prefix_entry.get().strip())
  126. self.cfg["rule_ip_limit"] = int(self.rule_limit_entry.get().strip())
  127. self.cfg["scan_interval"] = int(self.interval_entry.get().strip())
  128. except Exception as e:
  129. messagebox.showerror("错误", f"配置输入无效: {e}")
  130. return
  131. save_cfg(self.cfg)
  132. # try login on a background thread
  133. def _login():
  134. try:
  135. resp, sess_cookie = login()
  136. if sess_cookie:
  137. set_sess_key(sess_cookie)
  138. messagebox.showinfo("登录", f"登录完成, 状态: {resp.status_code}")
  139. except Exception as e:
  140. messagebox.showerror("登录失败", str(e))
  141. threading.Thread(target=_login, daemon=True).start()
  142. def on_select(self, evt):
  143. sel = self.listbox.curselection()
  144. if not sel:
  145. return
  146. idx = sel[0]
  147. items = protector_list.load_list()
  148. if idx >= len(items):
  149. return
  150. item = items[idx]
  151. self.ip_entry.delete(0, tk.END)
  152. self.ip_entry.insert(0, item.get("target_ip", ""))
  153. self.port_entry.delete(0, tk.END)
  154. self.port_entry.insert(0, str(item.get("src_port", "")))
  155. self.entry_threshold.delete(0, tk.END)
  156. self.entry_threshold.insert(0, str(item.get("threshold", "")))
  157. def add_entry(self):
  158. ip = self.ip_entry.get().strip()
  159. port = self.port_entry.get().strip()
  160. th = self.entry_threshold.get().strip() or None
  161. if not ip or not port:
  162. messagebox.showerror("错误", "ip 与 port 为必填")
  163. return
  164. try:
  165. entry = protector_list.add_entry(ip, int(port), int(th) if th else None)
  166. self.reload_listbox()
  167. messagebox.showinfo("添加", f"已添加: {entry}")
  168. except Exception as e:
  169. messagebox.showerror("错误", str(e))
  170. def update_entry(self):
  171. sel = self.listbox.curselection()
  172. if not sel:
  173. messagebox.showerror("错误", "先选择一条")
  174. return
  175. idx = sel[0]
  176. items = protector_list.load_list()
  177. if idx >= len(items):
  178. return
  179. eid = items[idx]["id"]
  180. ip = self.ip_entry.get().strip()
  181. port = self.port_entry.get().strip()
  182. th = self.entry_threshold.get().strip() or None
  183. try:
  184. updated = protector_list.update_entry(eid, target_ip=ip, src_port=int(port), threshold=(int(th) if th else None))
  185. if updated is None:
  186. messagebox.showerror("错误", "更新失败")
  187. else:
  188. self.reload_listbox()
  189. messagebox.showinfo("更新", f"已更新: {updated}")
  190. except Exception as e:
  191. messagebox.showerror("错误", str(e))
  192. def remove_entry(self):
  193. sel = self.listbox.curselection()
  194. if not sel:
  195. messagebox.showerror("错误", "先选择一条")
  196. return
  197. idx = sel[0]
  198. items = protector_list.load_list()
  199. if idx >= len(items):
  200. return
  201. eid = items[idx]["id"]
  202. ok = protector_list.remove_entry(eid)
  203. if ok:
  204. self.reload_listbox()
  205. messagebox.showinfo("删除", "已删除")
  206. else:
  207. messagebox.showerror("错误", "删除失败")
  208. def _update_countdown(self):
  209. """Update countdown label every second and trigger protector run when it reaches zero."""
  210. try:
  211. if not self.protector_runner:
  212. return
  213. # Update label
  214. # compute seconds until next run based on protector_runner's schedule
  215. try:
  216. if self.protector_runner.is_paused():
  217. self.countdown_var.set("Protector: paused")
  218. else:
  219. next_ts = self.protector_runner.get_next_run_time()
  220. if next_ts is None:
  221. # fallback to internal counter
  222. secs = max(0, int(self._countdown_seconds))
  223. else:
  224. secs = max(0, int(round(next_ts - time.time())))
  225. self.countdown_var.set(f"下一次检测:{secs}s")
  226. except Exception:
  227. self.countdown_var.set("Protector: running")
  228. # if protector hasn't been started yet, use internal countdown
  229. try:
  230. started = getattr(self.protector_runner, "_started", False)
  231. except Exception:
  232. started = False
  233. if not started:
  234. # use internal countdown to start
  235. if self._countdown_seconds <= 0:
  236. do_start = True
  237. else:
  238. do_start = False
  239. else:
  240. # protector already started; rely on its internal schedule
  241. do_start = False
  242. if do_start:
  243. # If protector runner not started yet, start its loop first
  244. try:
  245. if not getattr(self.protector_runner, "_started", False):
  246. # start the background loop
  247. self.protector_runner._started = True
  248. threading.Thread(target=self.protector_runner.start, daemon=True).start()
  249. except Exception:
  250. pass
  251. # ProtectorRunner.start() will perform an initial run, so do not
  252. # explicitly call run_once() here to avoid double execution.
  253. # reset internal countdown to interval after start
  254. try:
  255. self._countdown_seconds = int(self.protector_runner.get_interval())
  256. except Exception:
  257. self._countdown_seconds = self._initial_delay
  258. # reset countdown to configured interval
  259. try:
  260. self._countdown_seconds = int(self.protector_runner.get_interval())
  261. except Exception:
  262. self._countdown_seconds = self._initial_delay
  263. # update pause button text
  264. try:
  265. if self.pause_btn:
  266. self.pause_btn.config(text=("Resume" if self.protector_runner.is_paused() else "Pause"))
  267. except Exception:
  268. pass
  269. else:
  270. # when protector already started, we won't decrement internal counter
  271. # keep internal counter but keep ticking for display purposes
  272. if not started:
  273. self._countdown_seconds -= 1
  274. except Exception as e:
  275. # keep ticking even on error
  276. print("倒计时更新出错:", e)
  277. # schedule next tick
  278. try:
  279. self.after(1000, self._update_countdown)
  280. except Exception:
  281. pass
  282. def _toggle_pause(self):
  283. if not self.protector_runner:
  284. return
  285. try:
  286. if self.protector_runner.is_paused():
  287. self.protector_runner.resume()
  288. if self.pause_btn:
  289. self.pause_btn.config(text="Pause")
  290. else:
  291. self.protector_runner.pause()
  292. if self.pause_btn:
  293. self.pause_btn.config(text="Resume")
  294. except Exception as e:
  295. messagebox.showerror("错误", f"切换暂停/恢复失败: {e}")
  296. def run_gui(protector_runner=None):
  297. app = ProtectorGUI(protector_runner=protector_runner)
  298. app.mainloop()
  299. if __name__ == "__main__":
  300. run_gui()