# -*- coding: UTF-8 -*-
import os
import re
import time
import datetime
import configparser
import logging
from logging.handlers import TimedRotatingFileHandler
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
# 邮件发送配置。
# my_sender: QQ 发件邮箱。
# my_pass: QQ 邮箱 SMTP 授权码,不是网页登录密码。
# mail_dict: 告警接收人名单,key 为显示昵称,value 为邮箱地址。
my_sender='' # 发件人邮箱账号
my_pass = '' # 发件人邮箱密码
mail_dict = {“"}
# 收件人邮箱账号
# mail_dict = {"": ''} # 收件人邮箱账号
# 日志配置:日志文件统一写入脚本同级目录下的 log 子目录,便于计划任务运行时定位日志。
# 设置日志文件路径为相对路径
log_dir = os.path.join(os.path.dirname(__file__), 'log')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_handler = TimedRotatingFileHandler(
os.path.join(log_dir, 'check_monitem.log'),
when='midnight', # 每天零点切割:把昨天的日志归档为 check_monitem.log.日期
interval=1,
backupCount=7, # 最多保留 7 份历史日志(约一周),更早的自动删除
delay=True, # 首次写入时才创建文件
)
log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s: %(message)s'))
logging.basicConfig(level=logging.DEBUG, handlers=[log_handler])
logging.info('脚本开始执行')
# QQ 邮件发送函数。
# 作用:向单个收件人发送一封纯文本告警邮件。
# 返回值:发送成功返回 True,失败返回 False,失败原因写入日志。
def mail(receive_name, receive_user, title, mail_content):
ret=True
server=None
try:
msg=MIMEText(mail_content, 'plain', 'utf-8')
msg['From']=formataddr(["无双", my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['To']=formataddr([receive_name, receive_user]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['Subject'] = title # 邮件的主题,也可以说是标题
server=smtplib.SMTP_SSL("smtp.qq.com", 465) # 发件人邮箱中的SMTP服务器,端口是465(SSL)
server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码
server.sendmail(my_sender, [receive_user,], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
except smtplib.SMTPException as e: # 捕获SMTP相关异常
logging.error(f"邮件发送失败 - SMTP错误:{str(e)}")
ret=False
except Exception as e: # 捕获其他异常
logging.error(f"邮件发送失败 - 未知错误:{str(e)}")
ret=False
finally:
if server is not None:
try:
server.quit() # 无论成功与否都关闭连接,避免连接泄漏
except Exception as e:
logging.error(f"关闭SMTP连接失败:{str(e)}")
return ret
# 扫描 D 盘并筛选目标服务器的整体逻辑放在异常保护内:
# 若 D 盘不可访问或扫描过程出现异常,会发送告警邮件并终止,
# 避免监控脚本静默崩溃、收不到任何通知。
try:
res = os.listdir("D:/")
pattern = re.compile("^[Mm]irserver.*")
mir_list = {}
prefix = "D:/"
config = configparser.ConfigParser()
localtime_hour = time.localtime(time.time()).tm_hour
current_date = datetime.datetime.now().strftime("%m%d")
# 获取昨天的日期
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
yesterday_date = yesterday.strftime("%m%d")
# 获取前天的日期(用于一合服务器10点检查)
# day_before_yesterday = datetime.datetime.now() - datetime.timedelta(days=2)
# day_before_yesterday_date = day_before_yesterday.strftime("%m%d")
# print(day_before_yesterday_date)
# 遍历 D 盘根目录,筛选需要检查的 MirServer 目录。
# 过滤规则:
# 1. 目录名需匹配 Mirserver 开头;
# 2. 必须是实际存在的目录;
# 3. 排除测试/底板/模板目录;
# 4. 通过目录名中的日期 + Config.ini 中的区服名称,判断是否属于本次应检查的目标服。
for mydir in res:
# 排除测试模块目录,避免误报。
if "999-2" in mydir.lower() or "底板" in mydir or "模板" in mydir :
continue
mir_str = pattern.search(mydir)
if mir_str and os.path.isdir(prefix + mir_str.group()):
# 从服务器名称中提取日期(如Mirserver032801中的0328)
server_date_match = re.search(r'server(\d{4})', mydir.lower())
if not server_date_match:
logging.warning(f"服务器目录名称格式不正确:{mydir}")
continue
server_date = server_date_match.group(1)
# 获取服务器名称以判断区服类型
mir_conf = prefix + mir_str.group() + "/Config.ini"
if os.path.exists(mir_conf):
try:
config.clear()
config.read(mir_conf, encoding='gbk')
server_name = config.get('GameConf', 'GameName')
# 2区服务器使用昨天的日期匹配,1区使用当天日期
if "2区" in server_name and server_date == yesterday_date:
mir_list[mydir] = prefix + mir_str.group()
elif "1区" in server_name and server_date == current_date:
mir_list[mydir] = prefix + mir_str.group()
# 一合/统战服务器不限制目录日期,只要名称匹配即纳入检查
elif "一合" in server_name:
mir_list[mydir] = prefix + mir_str.group()
# elif "二合" in server_name:
# mir_list[mydir] = prefix + mir_str.group()
# elif "三合" in server_name:
# mir_list[mydir] = prefix + mir_str.group()
elif "统战" in server_name:
mir_list[mydir] = prefix + mir_str.group()
except configparser.Error as e:
logging.error(f"读取配置文件失败:{str(e)}")
continue
except Exception as e:
logging.error(f"处理服务器配置时发生未知错误:{str(e)}")
continue
except Exception as e:
logging.error(f"扫描D盘服务器目录失败:{str(e)}")
for name, addr in mail_dict.items():
mail_title = "服务器检查脚本异常"
mail_content = f"脚本运行出现异常:{str(e)}"
if mail(name, addr, mail_title, mail_content):
logging.info(f"已成功发送异常通知邮件给{name}")
else:
logging.error(f"发送异常通知邮件给{name}失败")
raise SystemExit(1) # 目录扫描失败属于致命错误,终止脚本避免误判继续执行
def get_drop_rate_folder(server_name):
"""根据服务器名称返回对应的爆率文件夹名称;无需检查的类型返回 False"""
if "1区" in server_name or "2区" in server_name:
return "A新区6小时爆率"
elif "一合" in server_name:
return "A一合爆率"
elif "统战" in server_name:
return "A统战爆率"
else:
return False # 其他类型的分区不进行爆率文件检查
# M2日志中爆率手动加载完成的关键字
RELOAD_KEYWORD = "怪物爆物品列表重加载完成"
def find_last_reload_time(mir_dir, after=None):
"""在 M2 日志中查找爆率重载完成时间。
参数说明:
- mir_dir: 单个服务器根目录,如 D:/Mirserver091501。
- after: 仅接受不早于该时间点的日志记录,用于限制只看本轮应检查时间窗内的重载动作。
返回值:
- 找到符合条件的重载时间则返回 datetime 对象;
- 未找到则返回 None。
"""
log_dir = os.path.join(mir_dir, "Mir200", "Log")
if not os.path.isdir(log_dir):
logging.warning(f"{mir_dir}的M2日志目录不存在:{log_dir}")
return None
ts_pat = re.compile(r"\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{2}:\d{2}")
cutoff = datetime.datetime.now() - datetime.timedelta(days=2)
names = [n for n in os.listdir(log_dir) if n.lower().endswith((".txt", ".log"))]
names.sort(key=lambda n: os.path.getmtime(os.path.join(log_dir, n)), reverse=True)
for fname in names:
fpath = os.path.join(log_dir, fname)
if datetime.datetime.fromtimestamp(os.path.getmtime(fpath)) < cutoff:
continue
try:
with open(fpath, "r", encoding="gbk", errors="ignore") as f:
for line in f:
if RELOAD_KEYWORD not in line:
continue
m = ts_pat.search(line)
if not m:
continue
try:
t = datetime.datetime.strptime(m.group(), "%Y/%m/%d %H:%M:%S")
except ValueError:
continue
if after is None or t >= after:
return t
except Exception as e:
logging.error(f"读取M2日志文件失败:{fpath},{str(e)}")
continue
return None
def check_drop_rate(mir_dir, server_name, after_hour, after_minute, check_times=3, check_reload=True):
"""检查爆率文件是否已替换,以及(可选)M2 是否在指定时间后重新加载。
检查逻辑:
1. 根据服务器名称确定对应的爆率文件夹;
2. 判断爆率文件是否存在;
3. 若存在且 check_reload 为 True,再检查 M2 日志里是否有“怪物爆物品列表重加载完成”;
check_reload 为 False 时(如一合/统战),文件已替换即视为正常,不校验加载;
4. 若异常,则发送告警邮件;
5. 默认最多检查 3 次,每次间隔 5 分钟。
"""
drop_rate_folder = get_drop_rate_folder(server_name)
# 如果返回False,则不进行爆率文件检查
if drop_rate_folder is False:
logging.info(f"{server_name}不需要进行爆率文件检查")
return
drop_rate_path = os.path.join(mir_dir, "Mir200", "Envir", "MonItems", drop_rate_folder)
# 构造本轮检查的时间门槛,只接受该时间之后的重载日志(仅 check_reload 为 True 时使用)。
reload_after = datetime.datetime.now().replace(hour=after_hour, minute=after_minute, second=0, microsecond=0)
for attempt in range(check_times):
logging.info(f"正在检查{server_name}的爆率文件,第{attempt + 1}次检查,路径:{drop_rate_path}")
if not os.path.exists(drop_rate_path):
logging.warning(f"{server_name}的爆率文件未替换:{drop_rate_path}")
mail_title = f"{server_name}爆率文件未替换"
mail_content = f"{server_name}爆率文件未替换,路径:{os.path.dirname(drop_rate_path)}"
else:
# 一合/统战等不校验 M2 手动加载,爆率文件已替换即视为正常
if not check_reload:
logging.info(f"{server_name}爆率文件已替换(不检查M2加载)")
return
reload_time = find_last_reload_time(mir_dir, after=reload_after)
if reload_time is not None:
logging.info(f"{server_name}爆率已加载({reload_after.strftime('%H:%M')}后记录:{reload_time})")
return
logging.warning(f"{server_name}爆率已替换但M2未加载({reload_after.strftime('%H:%M')}后无加载记录)")
mail_title = f"{server_name}爆率已替换但未加载"
mail_content = f"{server_name}爆率已替换,但{reload_after.strftime('%H:%M')}之后M2日志中没有加载记录,请点击M2菜单手动加载爆率"
for name, addr in mail_dict.items():
if mail(name, addr, mail_title, mail_content):
logging.info(f"已成功给{name}发送:{mail_title}")
else:
logging.error(f"发送{name}邮件失败")
if attempt < check_times - 1: # 最后一次检查不需要等待
time.sleep(300) # 等待5分钟(300秒)
try:
if not mir_list:
logging.warning("未找到符合条件的服务器目录")
# 对筛选出来的目标服务器逐个执行检查。
for mir_dir_name, mir_dir in mir_list.items():
if mir_dir.startswith(prefix):
mir_conf = mir_dir + "/" + "Config.ini"
try:
config.clear()
config.read(mir_conf, encoding='gbk')
server_name = config.get('GameConf', 'GameName')
# 1 区:当天 19 点触发检查,要求 18:50 之后应出现手动加载日志。
if server_name.find("1区") != -1 and localtime_hour == 19:
check_drop_rate(mir_dir, server_name, 18, 50)
# 2 区:次日 1 点触发检查,要求 00:50 之后应出现手动加载日志。
elif server_name.find("2区") != -1 and localtime_hour == 1:
check_drop_rate(mir_dir, server_name, 0, 50)
# 一合:17 点触发检查,仅检查昨天日期的服务器(当天的合区),只检查爆率文件是否替换。
elif server_name.find("一合") != -1 and localtime_hour == 17:
# 从目录名中提取日期
server_date_match = re.search(r'server(\d{4})', mir_dir_name.lower())
if server_date_match and server_date_match.group(1) == yesterday_date:
check_drop_rate(mir_dir, server_name, 16, 50, check_reload=False)
else:
logging.info(f"{server_name}不是当天的合区(前天日期),17点不进行检查")
# 统战:17 点触发检查,只检查爆率文件是否替换。
elif ("统战" in server_name) and localtime_hour == 17:
check_drop_rate(mir_dir, server_name, 16, 50, check_reload=False)
else:
# 非检查时段只记录日志,不发送邮件。
logging.info(f"当前时间{localtime_hour}点不在{server_name}的检查时间范围内")
except configparser.Error as e:
logging.error(f"读取配置文件失败:{str(e)}")
continue
except Exception as e:
logging.error(f"处理服务器配置时发生未知错误:{str(e)}")
continue
except Exception as e:
logging.error(f"警告处理错误:{str(e)}")
for name, addr in mail_dict.items():
mail_title = "服务器检查脚本异常"
mail_content = f"脚本运行出现异常:{str(e)}"
if mail(name, addr, mail_title, mail_content):
logging.info(f"已成功发送异常通知邮件给{name}")
else:
logging.error(f"发送异常通知邮件给{name}失败")
logging.info('脚本执行完成')