好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 再贱就再见 于 2026-8-21 01:26 编辑
找人写要450元,AI手搓了个公网IP变更邮箱提醒,设置完要点保存配置,可以最小化到托盘,软件同目录要有app.ico图标,才能最小化到系统托盘.
https://wwbmj.lanzouu.com/iYhTX43peyyf
用的过程中出现了些问题,现在更新到最新版了,应该不会出啥问题了...
[Python] 纯文本查看 复制代码 import requests
import smtplib
import time
import logging
import json
import os
import threading
import queue
import sys
import ctypes
from email.mime.text import MIMEText
from email.header import Header
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import winreg
import ipaddress
try:
from pystray import Icon, MenuItem, Menu
from PIL import Image, ImageDraw
HAS_TRAY = True
except ModuleNotFoundError:
HAS_TRAY = False
print("警告:未安装pystray pillow,托盘功能不可用,请执行 pip install requests pystray pillow")
# Windows互斥锁
MUTEX_NAME = "IPMonitor_Tool_20260820_UniqueMutex"
h_mutex = None
if sys.platform == "win32":
kernel32 = ctypes.windll.kernel32
h_mutex = kernel32.CreateMutexW(None, False, MUTEX_NAME)
last_err = kernel32.GetLastError()
if last_err == 183:
messagebox.showinfo("提示", "软件已经在运行中,请勿重复启动!")
sys.exit(0)
CONFIG_FILE = "config.json"
IPV4_SAVE_FILE = "last_ipv4.txt"
IPV6_SAVE_FILE = "last_ipv6.txt"
LOG_FILE = "ip_log.txt"
def get_app_dir():
"""获取exe/脚本所在目录(同目录)"""
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(sys.argv[0]))
def get_exe_fullpath():
if getattr(sys, 'frozen', False):
return sys.executable
else:
return os.path.abspath(sys.argv[0])
# 日志
logger = logging.getLogger("IPMonitor")
logger.setLevel(logging.INFO)
if not logger.handlers:
log_path = os.path.join(get_app_dir(), LOG_FILE)
fh = logging.FileHandler(log_path, encoding="utf-8")
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
fh.setFormatter(fmt)
logger.addHandler(fh)
def make_fallback_icon():
"""生成兜底托盘图标,内存绘制,不需要外部文件"""
img = Image.new("RGB", (64, 64), color=(20, 90, 180))
draw = ImageDraw.Draw(img)
draw.ellipse((12, 12, 52, 52), fill=(255, 255, 255))
return img
def is_valid_ipv4(ip_str: str) -> bool:
try:
ipaddress.IPv4Address(ip_str.strip())
return True
except (ipaddress.AddressValueError, ValueError):
return False
def is_valid_ipv6(ip_str: str) -> bool:
try:
ipaddress.IPv6Address(ip_str.strip())
return True
except (ipaddress.AddressValueError, ValueError):
return False
class IPMonitorGUI:
def __init__(self, root):
self.root = root
self.root.title("公网IP变更邮件提醒工具")
self.root.geometry("720x620")
self.running = False
self.monitor_thread = None
self.tray_icon = None
self.tray_thread = None
self.autostart_key_name = "IPMonitorTool"
self.log_queue = queue.Queue()
self.http_session = requests.Session()
self.http_session.trust_env = False
# 待发送告警缓存:保存未发送成功的IP变更邮件任务
self.pending_alert = None # {"subject":"xxx","body":"xxx"}
self.app_dir = get_app_dir()
self.CONFIG_FILE = os.path.join(self.app_dir, CONFIG_FILE)
self.IPV4_SAVE_FILE = os.path.join(self.app_dir, IPV4_SAVE_FILE)
self.IPV6_SAVE_FILE = os.path.join(self.app_dir, IPV6_SAVE_FILE)
self.LOG_FILE = os.path.join(self.app_dir, LOG_FILE)
self._poll_log_queue()
frame_cfg = ttk.LabelFrame(root, text="邮箱配置")
frame_cfg.pack(fill="x", padx=10, pady=5)
ttk.Label(frame_cfg, text="SMTP服务器:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
self.var_smtp = tk.StringVar(value="smtp.qq.com")
ttk.Entry(frame_cfg, textvariable=self.var_smtp, width=32).grid(row=0, column=1, padx=5, pady=3)
ttk.Label(frame_cfg, text="端口:").grid(row=0, column=2, padx=5, pady=3, sticky="w")
self.var_port = tk.StringVar(value="465")
ttk.Entry(frame_cfg, textvariable=self.var_port, width=10).grid(row=0, column=3, padx=5, pady=3)
ttk.Label(frame_cfg, text="发件邮箱:").grid(row=1, column=0, padx=5, pady=3, sticky="w")
self.var_sender = tk.StringVar()
ttk.Entry(frame_cfg, textvariable=self.var_sender, width=32).grid(row=1, column=1, padx=5, pady=3)
ttk.Label(frame_cfg, text="授权码:").grid(row=1, column=2, padx=5, pady=3, sticky="w")
self.var_pwd = tk.StringVar()
ttk.Entry(frame_cfg, textvariable=self.var_pwd, width=18, show="*").grid(row=1, column=3, padx=5, pady=3)
ttk.Label(frame_cfg, text="接收邮箱:").grid(row=2, column=0, padx=5, pady=3, sticky="w")
self.var_receiver = tk.StringVar()
ttk.Entry(frame_cfg, textvariable=self.var_receiver, width=32).grid(row=2, column=1, padx=5, pady=3)
ttk.Label(frame_cfg, text="检测间隔(秒):").grid(row=2, column=2, padx=5, pady=3, sticky="w")
self.var_interval = tk.StringVar(value="300")
ttk.Entry(frame_cfg, textvariable=self.var_interval, width=10).grid(row=2, column=3, padx=5, pady=3)
frame_adv = ttk.LabelFrame(root, text="高级设置")
frame_adv.pack(fill="x", padx=10, pady=5)
ttk.Label(frame_adv, text="日志保留天数:").grid(row=0, column=0, padx=5, pady=3, sticky="w")
self.var_log_days = tk.StringVar(value="30")
ttk.Entry(frame_adv, textvariable=self.var_log_days, width=3).grid(row=0, column=1, padx=5, pady=3)
ttk.Label(frame_adv, text="(到期自动清理旧日志)").grid(row=0, column=2, padx=2, pady=3, sticky="w")
self.var_monitor_ipv6 = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_adv, text="同时监控IPv6", variable=self.var_monitor_ipv6).grid(row=0, column=3, padx=8, pady=3)
frame_boot = ttk.Frame(frame_adv)
frame_boot.grid(row=1, column=0, columnspan=5, padx=5, pady=5)
self.btn_clean_log = ttk.Button(frame_boot, text="手动清理日志", command=self.manual_clean_log)
self.btn_clean_log.grid(row=0, column=0, padx=3)
self.btn_toggle_monitor = ttk.Button(frame_boot, text="开始监控", command=self.toggle_monitor)
self.btn_toggle_monitor.grid(row=0, column=1, padx=3)
self.btn_autostart = ttk.Button(frame_boot, text="开启开机自启", command=self.toggle_auto_start)
self.btn_autostart.grid(row=0, column=2, padx=3)
self.btn_save = ttk.Button(frame_boot, text="保存配置", command=self.save_config)
self.btn_save.grid(row=0, column=3, padx=3)
frame_log = ttk.LabelFrame(root, text="运行日志")
frame_log.pack(fill="both", expand=True, padx=10, pady=5)
self.log_text = scrolledtext.ScrolledText(frame_log, height=14)
self.log_text.pack(fill="both", expand=True, padx=5, pady=5)
self.update_autostart_btn_text()
self.update_monitor_button_text()
self.init_tray()
self.root.protocol("WM_DELETE_WINDOW", self.on_window_close)
self.load_config()
cfg = self.read_raw_config()
if cfg.get("monitor_running", False):
self.log("检测到上次监控为开启状态,自动启动监控")
self.root.after(300, self.start_monitor_inner)
def update_monitor_button_text(self):
if self.running:
self.btn_toggle_monitor.config(text="停止监控")
else:
self.btn_toggle_monitor.config(text="开始监控")
def read_raw_config(self):
if os.path.exists(self.CONFIG_FILE):
try:
with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
return {}
def is_autostart_enabled(self):
try:
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ)
val, _ = winreg.QueryValueEx(key, self.autostart_key_name)
winreg.CloseKey(key)
return True
except FileNotFoundError:
return False
def update_autostart_btn_text(self):
if self.is_autostart_enabled():
self.btn_autostart.config(text="关闭开机自启")
else:
self.btn_autostart.config(text="开启开机自启")
def toggle_auto_start(self):
exe_full = get_exe_fullpath()
exe_dir = get_app_dir()
is_exe = getattr(sys, 'frozen', False)
if not os.path.exists(exe_full):
messagebox.showerror("错误", "程序路径不存在,无法设置自启!")
return
try:
if self.is_autostart_enabled():
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_WRITE)
winreg.DeleteValue(key, self.autostart_key_name)
winreg.CloseKey(key)
messagebox.showinfo("提示", "已关闭开机自启")
self.log("✅已关闭Windows开机自启")
else:
if not is_exe:
messagebox.showwarning("警告", "当前直接运行py源码,开机自启不会正常工作!\n请打包为exe程序后再开启自启。")
cmd_str = f'cmd /c "cd /d "{exe_dir}" && start "" "{exe_full}" "'
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_WRITE)
winreg.SetValueEx(key, self.autostart_key_name, 0, winreg.REG_SZ, cmd_str)
winreg.CloseKey(key)
messagebox.showinfo("提示", f"已开启开机自启")
self.log(f"✅已开启Windows开机自启,exe路径:{exe_full}")
self.update_autostart_btn_text()
except Exception as e:
messagebox.showerror("错误", f"操作开机自启失败:{e}")
self.log(f"开机自启操作异常:{e}")
def toggle_monitor(self):
if self.running:
self.stop_monitor_inner()
else:
self.start_monitor_inner()
def start_monitor_inner(self):
if self.running:
messagebox.showwarning("提示", "监控已经在运行!")
return
try:
port = int(self.var_port.get())
interval = int(self.var_interval.get())
log_days = int(self.var_log_days.get())
if interval <= 0 or log_days <= 0:
raise ValueError
except Exception:
messagebox.showerror("错误", "端口、检测间隔、日志保留天数必须填写正整数!")
return
self.running = True
self.save_config(monitor_running=True)
self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
self.monitor_thread.start()
self.update_monitor_button_text()
def stop_monitor_inner(self):
self.running = False
self.pending_alert = None
self.save_config(monitor_running=False)
self.update_monitor_button_text()
def init_tray(self):
if not HAS_TRAY:
self.log("pystray未安装,托盘功能禁用")
return
icon_img = None
local_ico = os.path.join(self.app_dir, "app.ico")
try:
if os.path.exists(local_ico):
icon_img = Image.open(local_ico)
self.log(f"✅成功加载【程序同目录】app.ico托盘图标: {local_ico}")
else:
self.log(f"⚠️同目录未找到app.ico({local_ico}),使用程序内置兜底托盘图标")
icon_img = make_fallback_icon()
except Exception as e:
self.log(f"⚠️加载app.ico失败,切换内置兜底图标:{e}")
icon_img = make_fallback_icon()
menu = Menu(
MenuItem("显示主窗口", self.show_window, default=True),
MenuItem("彻底退出", self.quit_app)
)
try:
self.tray_icon = Icon("IPMonitor", icon_img, "IP监控工具", menu)
self.tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
self.tray_thread.start()
self.log("✅系统托盘初始化完成")
except Exception as e:
self.log(f"❌托盘启动失败:{e}")
def on_window_close(self):
if HAS_TRAY and self.tray_icon:
self.root.withdraw()
else:
self.quit_app()
def show_window(self, icon=None, item=None):
self.root.after(0, lambda: (self.root.deiconify(), self.root.lift()))
def quit_app(self):
self.log("正在执行程序退出...")
self.running = False
self.pending_alert = None
time.sleep(0.2)
try:
self.http_session.close()
except Exception:
pass
if HAS_TRAY and self.tray_icon:
try:
self.tray_icon.stop()
except Exception:
pass
try:
self.root.destroy()
except Exception:
pass
global h_mutex
if h_mutex is not None:
try:
ctypes.windll.kernel32.ReleaseMutex(h_mutex)
ctypes.windll.kernel32.CloseHandle(h_mutex)
except Exception:
pass
sys.exit(0)
def _poll_log_queue(self):
while not self.log_queue.empty():
msg = self.log_queue.get()
self.log_text.insert(tk.END, f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n")
self.log_text.see(tk.END)
self.root.after(100, self._poll_log_queue)
def log(self, msg):
self.log_queue.put(msg)
logger.info(msg)
def auto_clean_log(self, keep_days: int, max_size_mb=20):
if keep_days <= 0:
return
if not os.path.exists(self.LOG_FILE):
return
try:
fsize = os.path.getsize(self.LOG_FILE)
max_byte = max_size_mb * 1024 * 1024
if fsize > max_byte:
self.log(f"⚠️日志文件超过{max_size_mb}MB,执行截断")
with open(self.LOG_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = lines[len(lines) // 2:]
with open(self.LOG_FILE, "w", encoding="utf-8") as f:
f.writelines(new_lines)
now_ts = time.time()
keep_sec = keep_days * 86400
new_lines = []
with open(self.LOG_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
keep_line = True
try:
time_str = line.split(" - ", 1)[0]
log_time = time.strptime(time_str, "%Y-%m-%d %H:%M:%S")
log_ts = time.mktime(log_time)
if now_ts - log_ts > keep_sec:
keep_line = False
except Exception:
pass
if keep_line:
new_lines.append(line)
with open(self.LOG_FILE, "w", encoding="utf-8") as f:
f.writelines(new_lines)
return True
except Exception as e:
self.log(f"⚠️日志清理异常:{e}")
return False
def manual_clean_log(self):
try:
days = int(self.var_log_days.get().strip())
if days <= 0:
messagebox.showerror("错误", "日志保留天数必须大于0")
return
self.log_text.delete(1.0, tk.END)
ok = self.auto_clean_log(days)
if ok:
messagebox.showinfo("完成", f"已清理,保留最近 {days} 天日志")
self.log(f"✅手动执行日志清理,保留{days}天")
except Exception as e:
messagebox.showerror("错误", f"清理失败:{e}")
def load_config(self):
if os.path.exists(self.CONFIG_FILE):
try:
with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
cfg = json.load(f)
self.var_smtp.set(cfg.get("smtp_server", "smtp.qq.com"))
self.var_port.set(cfg.get("smtp_port", "465"))
self.var_sender.set(cfg.get("sender_email", ""))
self.var_pwd.set(cfg.get("sender_pwd", ""))
self.var_receiver.set(cfg.get("receiver_email", ""))
self.var_interval.set(cfg.get("check_interval", "300"))
self.var_log_days.set(cfg.get("log_keep_days", "30"))
self.var_monitor_ipv6.set(cfg.get("monitor_ipv6", False))
self.log("已读取本地保存的配置")
except Exception as e:
self.log(f"读取配置失败: {e}")
def save_config(self, monitor_running=None):
try:
port = int(self.var_port.get().strip())
interval = int(self.var_interval.get().strip())
log_days = int(self.var_log_days.get().strip())
if interval <= 0 or log_days <= 0:
raise ValueError("数值必须大于0")
old_cfg = self.read_raw_config()
cfg = {
"smtp_server": self.var_smtp.get().strip(),
"smtp_port": port,
"sender_email": self.var_sender.get().strip(),
"sender_pwd": self.var_pwd.get().strip(),
"receiver_email": self.var_receiver.get().strip(),
"check_interval": interval,
"log_keep_days": log_days,
"monitor_ipv6": self.var_monitor_ipv6.get(),
"monitor_running": monitor_running if monitor_running is not None else old_cfg.get("monitor_running", False)
}
with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
self.log("✅ 配置保存成功!")
except Exception as e:
self.log(f"❌ 保存配置失败: {e}")
messagebox.showerror("错误", f"保存失败:{e}")
def get_ipv4(self):
api_list = [
"https://api.ipify.org",
"https://ip.3322.net",
"https://ifconfig.me/ip"
]
for api in api_list:
try:
resp = self.http_session.get(api, timeout=8)
ip = resp.text.strip()
if ip and is_valid_ipv4(ip):
return ip
except Exception:
continue
return None
def get_ipv6(self):
api_list = [
"https://ipv6.icanhazip.com",
"https://ifconfig.me/ipv6"
]
for api in api_list:
try:
resp = self.http_session.get(api, timeout=8)
ip = resp.text.strip()
if ip and is_valid_ipv6(ip):
return ip
except Exception:
continue
return None
def read_saved_ip(self, filepath):
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read().strip()
except (FileNotFoundError, PermissionError, OSError):
return None
def save_saved_ip(self, filepath, ip):
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(ip)
except (PermissionError, OSError) as e:
self.log(f"⚠️保存IP文件失败 {filepath} : {e}")
def send_mail(self, subject, content, cfg):
msg = MIMEText(content, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = cfg["sender_email"]
msg["To"] = cfg["receiver_email"]
try:
with smtplib.SMTP_SSL(cfg["smtp_server"], cfg["smtp_port"], timeout=10) as server:
server.login(cfg["sender_email"], cfg["sender_pwd"])
server.sendmail(cfg["sender_email"], cfg["receiver_email"], msg.as_string())
return True
except Exception as e:
self.log(f"❌邮件发送失败:{e}")
return False
def monitor_loop(self):
cfg = {
"smtp_server": self.var_smtp.get().strip(),
"smtp_port": int(self.var_port.get().strip()),
"sender_email": self.var_sender.get().strip(),
"sender_pwd": self.var_pwd.get().strip(),
"receiver_email": self.var_receiver.get().strip(),
"check_interval": int(self.var_interval.get().strip()),
"log_keep_days": int(self.var_log_days.get().strip()),
"monitor_ipv6": self.var_monitor_ipv6.get()
}
if not cfg["sender_email"] or not cfg["sender_pwd"] or not cfg["receiver_email"]:
self.log("❌ 邮箱配置不全,请填写完整邮箱、授权码!")
self.running = False
self.root.after(0, self.update_monitor_button_text)
return
self.log("==== IP监控已启动 ====")
self.auto_clean_log(cfg["log_keep_days"])
self.pending_alert = None
ipv4_now = self.get_ipv4()
ipv6_now = self.get_ipv6() if cfg["monitor_ipv6"] else None
start_content = "IP监控程序已成功启动!\n"
if ipv4_now:
start_content += f"当前IPv4:{ipv4_now}\n"
old4 = self.read_saved_ip(self.IPV4_SAVE_FILE)
if old4 is None:
self.save_saved_ip(self.IPV4_SAVE_FILE, ipv4_now)
self.log("首次运行记录IPv4")
else:
self.log("⚠️启动获取IPv4失败")
if cfg["monitor_ipv6"]:
if ipv6_now:
start_content += f"当前IPv6:{ipv6_now}\n"
old6 = self.read_saved_ip(self.IPV6_SAVE_FILE)
if old6 is None:
self.save_saved_ip(self.IPV6_SAVE_FILE, ipv6_now)
self.log("首次运行记录IPv6")
else:
self.log("⚠️启动获取IPv6失败(无IPv6网络属于正常)")
start_content += "后续IP发生变动将会推送邮件通知"
def async_send_start_mail():
ok = self.send_mail("【IP监控启动通知】", start_content, cfg)
if ok:
self.log("✅启动测试邮件发送成功,请查收邮箱!")
else:
self.log("⚠️启动通知邮件发送失败,不再重试")
threading.Thread(target=async_send_start_mail, daemon=True).start()
ipv4_fail_cnt = 0
ipv4_max_fail_log = 5
ipv6_fail_cnt = 0
ipv6_max_fail_log = 5
while self.running:
# 优先重试上一轮遗留的告警任务
if self.pending_alert is not None:
job = self.pending_alert
self.log(f"⏳重试未发送告警邮件 [{job['subject']}]")
send_ok = self.send_mail(job["subject"], job["body"], cfg)
if send_ok:
self.log(f"✅告警邮件重试发送成功,清除待发送任务")
self.pending_alert = None
else:
self.log(f"⚠️告警邮件重试依旧失败,等待下一轮检测间隔")
ipv4_now = self.get_ipv4()
ipv6_now = self.get_ipv6() if cfg["monitor_ipv6"] else None
if ipv4_now:
ipv4_fail_cnt = 0
old4 = self.read_saved_ip(self.IPV4_SAVE_FILE)
self.log(f"IPv4: {ipv4_now} | 上次:{old4}")
if old4 is not None and ipv4_now != old4:
self.log("⚠️IPv4发生变更!立即尝试发送通知邮件")
subj = "【公网IPv4变更提醒】"
body = f"检测到公网IPv4变更\n旧IPv4:{old4}\n新IPv4:{ipv4_now}"
# IP变更立刻发送一次
send_ok = self.send_mail(subj, body, cfg)
if send_ok:
self.log("✅IP变更邮件发送成功")
self.pending_alert = None
else:
self.log("⚠️IP变更即时发送失败,加入待发送队列,后续周期自动重试")
self.pending_alert = {"subject": subj, "body": body}
self.save_saved_ip(self.IPV4_SAVE_FILE, ipv4_now)
else:
ipv4_fail_cnt += 1
if ipv4_fail_cnt % ipv4_max_fail_log == 0:
self.log(f"⚠️连续{ipv4_fail_cnt}次获取IPv4失败")
if cfg["monitor_ipv6"]:
if ipv6_now:
ipv6_fail_cnt = 0
old6 = self.read_saved_ip(self.IPV6_SAVE_FILE)
self.log(f"IPv6: {ipv6_now} | 上次:{old6}")
if old6 is not None and ipv6_now != old6:
self.log("⚠️IPv6发生变更!立即尝试发送通知邮件")
subj = "【公网IPv6变更提醒】"
body = f"检测到公网IPv6变更\n旧IPv6:{old6}\n新IPv6:{ipv6_now}"
send_ok = self.send_mail(subj, body, cfg)
if send_ok:
self.log("✅IP变更邮件发送成功")
self.pending_alert = None
else:
self.log("⚠️IP变更即时发送失败,加入待发送队列,后续周期自动重试")
self.pending_alert = {"subject": subj, "body": body}
self.save_saved_ip(self.IPV6_SAVE_FILE, ipv6_now)
else:
ipv6_fail_cnt += 1
if ipv6_fail_cnt % ipv6_max_fail_log == 0:
self.log(f"⚠️连续{ipv6_fail_cnt}次获取IPv6失败(无IPv6网络可忽略)")
sleep_cnt = 0
total_sleep = cfg["check_interval"]
while sleep_cnt < total_sleep and self.running:
time.sleep(1)
sleep_cnt += 1
self.log("==== IP监控已停止 ====")
self.root.after(0, self.update_monitor_button_text)
if __name__ == "__main__":
root = tk.Tk()
local_ico = os.path.join(get_app_dir(), "app.ico")
try:
if os.path.exists(local_ico):
root.iconbitmap(local_ico)
except Exception:
pass
app = IPMonitorGUI(root)
try:
root.mainloop()
except KeyboardInterrupt:
app.quit_app()
|
|