diff --git a/.gitignore b/.gitignore index 4940382..36f4010 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dmypy.json # 自定义 result -output \ No newline at end of file +output +.idea/* \ No newline at end of file diff --git a/README.md b/README.md index 5ab655c..416932f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ 2. 哪些进程在你不用的时候突然占用大量资源 3. 监听系统进程资源占用情况 4. 关注指定程序的资源变动情况 +5. 通过邮件将监控结果发送给用户 理论上该程序跨平台,我在编码的时候也尽力使用兼容性更强的代码,所以即便出现了兼容性问题也应该很容易定位和解决。 @@ -39,7 +40,7 @@ python start.py -h ```text $ python start.py -h -usage: start.py [-h] [-o OUTPUT] [-i INTERVAL] [-f [FILTER ...]] +usage: start.py [-h] [-o OUTPUT] [-i INTERVAL] [-f [FILTER ...]] [-m] optional arguments: -h, --help show this help message and exit @@ -49,6 +50,17 @@ optional arguments: 监听时间间隔(毫秒) -f [FILTER ...], --filter [FILTER ...] 限制监听范围,为进程名(如explorer.exe) + -m False/True, --mail False/True + 是否使用邮件通知 + -a ADDRESS, --address ADDRESS + 邮件发送方地址 + -n NAME, --name NAME + 邮件发送方地址 + -p PASSWORD, --password PASSWORD + 邮件发送方密钥 + -r RECEIVER, --receiver RECEIVER + 邮件接收方地址 + ``` #### 监听所有的进程 @@ -89,6 +101,14 @@ python start.py -f notepad.exe Taskmgr.exe -i 1000 默认情况下会在当前目录的`output`目录下生成结果,里面主要有两项:`summury.txt`文件和`process`文件夹。前者主要记录了一些宏观统计信息。后者则包含了众多被监听的进程数据。数据以`html`的形式提供,每个文件名均为`进程名.html`格式,每个网页内包含了三张图表,可自行查看。 +#### 邮件通知 + +该功能基于[iMail](https://github.com/mtics/iMail) + +- 首先需要用户通过指令`-m True`或者`--mail True`来允许使用邮件通知功能 +- 然后用户需要通过指令进行相关配置,详情请见代码。 +- 目前通知会在用户停止监听后,将相关输出发送至指定邮箱。 + ## 如何贡献 你可以提交`Issue`或者`Fork+PR`的方式提出你的想法和修改,我会不定期的查看并作出修改。当然,你也可以完全根据项目所属协议进行合法操作。 diff --git a/common.py b/SRM/common.py similarity index 63% rename from common.py rename to SRM/common.py index 8bf55e8..c54081e 100644 --- a/common.py +++ b/SRM/common.py @@ -1,12 +1,11 @@ - # Bytes -> KB MB GB def trans_B2KB(val): return float(val) / 1024 def trans_B2MB(val): - return trans_B2KB(val)/1024 + return trans_B2KB(val) / 1024 def trans_B2GB(val): - return trans_B2MB(val)/1024 + return trans_B2MB(val) / 1024 diff --git a/exporter.py b/SRM/exporter.py similarity index 56% rename from exporter.py rename to SRM/exporter.py index 919951a..82d8b8d 100644 --- a/exporter.py +++ b/SRM/exporter.py @@ -1,7 +1,26 @@ +import datetime +import os + from pyecharts import charts from pyecharts import options as opts -import os -import datetime + + +def resource_chart(page_title, title, subtitle, x_data, y_data, y_label=''): + line_chart = charts.Line( + opts.InitOpts(page_title=page_title) + ).set_global_opts( + title_opts=opts.TitleOpts(title=title, subtitle=subtitle) + ) + + line_chart.add_xaxis(xaxis_data=x_data) + line_chart.add_yaxis(y_label, + y_axis=y_data, + markline_opts=opts.MarkLineOpts( + data=[opts.MarkLineItem(type_="max"), opts.MarkLineItem(type_="average")]), + label_opts=opts.LabelOpts(is_show=False) + ) + + return line_chart def exportCharts(process_name, data, output_path): @@ -9,26 +28,18 @@ def exportCharts(process_name, data, output_path): layout=charts.Page.DraggablePageLayout) page.page_title = "{} 统计信息".format(process_name) - line_cpu = charts.Line( - opts.InitOpts(page_title="{} CPU占用(%)".format(process_name)))\ - .set_global_opts(title_opts=opts.TitleOpts(title="CPU占用信息", subtitle="单位: %")) - line_cpu.add_xaxis(xaxis_data=data["Time"]) - line_cpu.add_yaxis("CPU占用(%)", y_axis=data["CPU"], markline_opts=opts.MarkLineOpts( - data=[opts.MarkLineItem(type_="max"), opts.MarkLineItem(type_="average")]), label_opts=opts.LabelOpts(is_show=False)) - - line_mem = charts.Line( - opts.InitOpts(page_title="{} 内存占用(MB)".format(process_name)))\ - .set_global_opts(title_opts=opts.TitleOpts(title="内存占用信息", subtitle="单位: MB")) - line_mem.add_xaxis(xaxis_data=data["Time"]) - line_mem.add_yaxis("内存(MB)", y_axis=data["MEM"], markline_opts=opts.MarkLineOpts( - data=[opts.MarkLineItem(type_="max"), opts.MarkLineItem(type_="average")]), label_opts=opts.LabelOpts(is_show=False)) - - line_io = charts.Line( - opts.InitOpts(page_title="{} IO写(MB)".format(process_name)))\ - .set_global_opts(title_opts=opts.TitleOpts(title="IO信息", subtitle="单位: MB")) - line_io.add_xaxis(xaxis_data=data["Time"]) - line_io.add_yaxis("IO写(MB)", y_axis=data["IO"], markline_opts=opts.MarkLineOpts( - data=[opts.MarkLineItem(type_="max"), opts.MarkLineItem(type_="average")]), label_opts=opts.LabelOpts(is_show=False)) + + line_cpu = resource_chart(page_title="{} CPU占用(%)".format(process_name), + title="CPU占用信息", subtitle="单位: %", + x_data=data["Time"], y_data=data["CPU"], y_label="CPU占用(%)") + + line_mem = resource_chart(page_title="{} 内存占用(MB)".format(process_name), + title="内存占用信息", subtitle="单位: MB", + x_data=data["Time"], y_data=data["MEM"], y_label="内存(MB)") + + line_io = resource_chart(page_title="{} IO写(MB)".format(process_name), + title="IO信息", subtitle="单位: MB", + x_data=data["Time"], y_data=data["IO"], y_label="IO写(MB)") page.add(line_cpu, line_mem, line_io) page.render(os.path.join(output_path, "{}.html".format(process_name))) @@ -36,7 +47,7 @@ def exportCharts(process_name, data, output_path): def export(data_dict: dict, output_path: str, interval): process_dat_path = os.path.join(output_path, "process") - summury_txt_path = os.path.join(output_path, "summury.txt") + summary_txt_path = os.path.join(output_path, "summary.txt") os.makedirs(process_dat_path, exist_ok=True) @@ -50,8 +61,8 @@ def export(data_dict: dict, output_path: str, interval): stat = pm.getStatistic() exportCharts(process_name, stat, process_dat_path) - # 统计summury - if(stat["END_TIME"] != None): + # 统计summary + if stat["END_TIME"] != None: close_process_set.add(process_name) if stop_timestamp < stat["END_TIME"]: @@ -67,7 +78,7 @@ def export(data_dict: dict, output_path: str, interval): # 之后启动的 start_process_set.add(process_name) - with open(summury_txt_path, "w") as f: + with open(summary_txt_path, "w", encoding='utf-8') as f: f.write("======== 汇总 ========\n") f.write("报告时间:{}\n".format(datetime.datetime.now())) f.write("统计时间段:{} - {}\n".format(start_timestamp, stop_timestamp)) diff --git a/SRM/mail.py b/SRM/mail.py new file mode 100644 index 0000000..f5c7fc3 --- /dev/null +++ b/SRM/mail.py @@ -0,0 +1,65 @@ +import glob +import os + +import iMail + + +class MAIL(object): + + def __init__(self, args): + + # the information for enable mailing + self.__sender = { + 'host': args.mail_server, + 'address': args.sender_addr, + 'name': args.sender_name, + 'pwd': args.pwd, + 'receivers': args.receivers, + } + + self.__output_path = args.output # the path of outputs + self.__enable = args.mail # whether the mailing system is enabled + + def send_mail(self, subject='监控统计', content='', attachments=None): + """ + Package process monitoring results and send them to users via email + """ + + # Create an email object for iMail + mail_system = iMail.EMAIL(host=self.__sender['host'], sender_addr=self.__sender['address'], + pwd=self.__sender['pwd'], sender_name=self.__sender['name']) + + # Set the receiver list + mail_system.set_receiver(self.__sender['receivers']) + + # New an email + mail_system.new_mail(subject=subject, encoding='utf-8') + + # If user does not set the content, + # use the content of 'summary.txt'; + # Else, attach the file and sent it through email + if content == '': + with open(os.path.join(self.__output_path, 'summary.txt'), 'r', encoding='utf-8') as file: + mail_system.add_text(content=file.read()) + else: + mail_system.add_text(content=content) + mail_system.attach_files(os.path.join(self.__output_path, 'summary.txt')) + + # Attach all output files or the specified files + if attachments == None: + files = glob.glob(os.path.join(self, 'process/*')) + mail_system.attach_files(files) + else: + mail_system.attach_files(attachments) + + mail_system.send_mail() + + def mail(self, subject='监控统计', content='', attachments=None): + + if self.__enable: + print("邮件发送中,请等待...") + try: + self.send_mail(subject, content, attachments) + print("邮件发送成功") + except Exception as e: + print("邮件发送失败: {},请自行查看".format(e)) diff --git a/monitor.py b/SRM/monitor.py similarity index 90% rename from monitor.py rename to SRM/monitor.py index 4428644..e3e1c5a 100644 --- a/monitor.py +++ b/SRM/monitor.py @@ -1,11 +1,12 @@ -import psutil -import common +import datetime +import platform import threading import time from collections import defaultdict -import datetime -import exporter -import platform + +import psutil + +from SRM import common, exporter def showSystemInfo(): @@ -30,9 +31,9 @@ def showSystemInfo(): class ProcessMonitor: - ''' + """ 进程监视器,根据进程名归并,即将子进程并入父进程计算 - ''' + """ def __init__(self, process_name: str) -> None: self.__process_name = process_name @@ -67,7 +68,7 @@ def getStatistic(self): def __monitorThread(self): process_map = {} - while(self.__is_running): + while self.__is_running: pid_num = 0 cpu_loaded = 0.0 mem = 0.0 @@ -79,7 +80,7 @@ def __monitorThread(self): try: process_map[pid] = psutil.Process(pid=pid) process = process_map[pid] - if(process.name() != self.__process_name): + if process.name() != self.__process_name: # 认为pid被复用了 continue except psutil.NoSuchProcess as e: @@ -103,7 +104,7 @@ def __monitorThread(self): process_map.clear() self.__recordStat(cpu_loaded, mem, io) - time.sleep(self.__interval/1000) + time.sleep(self.__interval / 1000) def __clearStatistic(self): self.__statistic.clear() @@ -128,11 +129,11 @@ def __init__(self) -> None: self.__thread = None self.__process_map = defaultdict(ProcessMonitor) # 存放各种进程管理器 - def start(self, interval, filter=set()): + def start(self, interval, thread_filter=set()): self.__is_running = True self.__interval = interval self.__thread = threading.Thread( - target=self.__threadFunc, args=(set(filter),)) + target=self.__threadFunc, args=(set(thread_filter),)) self.__thread.start() def export(self, data_path): @@ -144,18 +145,18 @@ def stop(self): self.__is_running = False self.__thread.join() - def __threadFunc(self, filter): - ''' + def __threadFunc(self, thread_filter): + """ 子线程函数,用于扫描系统所有的进程,并且归类。再将进程分发给子进程进行监听 - ''' + """ # 开启子进程 - while(self.__is_running): + while self.__is_running: for process_name, ids in self.__getProcessesInfo().items(): - if(len(filter) > 0 and process_name not in filter): + if len(thread_filter) > 0 and process_name not in thread_filter: continue self.__distributeProcessName(process_name, ids) - time.sleep(self.__interval/1000) + time.sleep(self.__interval / 1000) # 取消所有线程和子进程 for _, pm in self.__process_map.items(): @@ -184,6 +185,6 @@ def __distributeProcessName(self, process_name: str, ids: set): self.__process_map.get(process_name).setPidSet(ids) def __processMonitorFunc(self): - while(self.__is_running): + while self.__is_running: pass # 取消所有监听 diff --git a/requirements.txt b/requirements.txt index 7e72d33..c998435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ psutil -pyecharts \ No newline at end of file +pyecharts +iMail \ No newline at end of file diff --git a/start.py b/start.py index 802ceaa..a619935 100644 --- a/start.py +++ b/start.py @@ -1,48 +1,80 @@ -import os +# -*- coding: UTF-8 -*- + import argparse -import monitor -import signal +import os import shutil +import signal + +from SRM import monitor, mail -argparser = argparse.ArgumentParser() -argparser.add_argument("-o", "--output", type=str, - help="输出位置", required=False, default="./output") -argparser.add_argument("-i", "--interval", type=int, - help="监听时间间隔(毫秒)", required=False, default=5000) -argparser.add_argument("-f", "--filter", nargs='*', - help="限制监听范围,为进程名(如explorer.exe)", default=[]) -args = argparser.parse_args() +def argParser(): + """ + Set the argument parser + """ -shutil.rmtree(args.output, ignore_errors=True) + argparser = argparse.ArgumentParser() -# 输出系统信息 -monitor.showSystemInfo() -# 启动监听 -sys_monitor = monitor.SysMonitor() -is_running_flag = True + argparser.add_argument("-o", "--output", type=str, + help="输出位置", required=False, default="./output") + argparser.add_argument("-i", "--interval", type=int, + help="监听时间间隔(毫秒)", required=False, default=5000) + argparser.add_argument("-f", "--filter", nargs='*', + help="限制监听范围,为进程名(如explorer.exe)", default=[]) -## 设定信号捕获 + # args for mailing + argparser.add_argument("-m", "--mail", type=bool, + help="是否发送邮件通知", default=False) + argparser.add_argument("-a", "--address", type=str, + help="邮件发送方地址", default='') + argparser.add_argument("-n", "--name", type=str, + help="邮件发送方名称", default='no_reply') + argparser.add_argument("-p", "--password", type=str, + help="邮件发送方密钥", default='') + argparser.add_argument("-r", "--receiver", type=str, + help="邮件接收方地址", default='') + return argparser.parse_args() + +# 设定信号捕获 def onSignalInterHandler(signum, frame): global is_running_flag - print("正在停止...(整理数据中,可能需要等待一段时间)") + print("Stopping...") + print("Data handling in progress, please wait for a while...") is_running_flag = False -signal.signal(signal.SIGINT, onSignalInterHandler) +if __name__ == '__main__': + + # Calling the parser to set inputs + args = argParser() + + shutil.rmtree(args.output, ignore_errors=True) + + # 输出系统信息 + monitor.showSystemInfo() + # 启动监听 + sys_monitor = monitor.SysMonitor() + is_running_flag = True + + signal.signal(signal.SIGINT, onSignalInterHandler) + + # 开启监听线程(fake线程) + sys_monitor.start(args.interval, args.filter) + print("\n正在监听... 键盘敲击[CTRL+C]即可结束监听并进行统计...") + while is_running_flag: + import time + + time.sleep(1) + + sys_monitor.stop() + print("监听已停止...正在导出数据...") + sys_monitor.export(args.output) + + print("结果输出到:", os.path.abspath(args.output)) -## 开启监听线程(fake线程) -sys_monitor.start(args.interval, args.filter) -print() -print("正在监听... 键盘敲击[Ctrt+c]即可结束统计...") -while(is_running_flag): - import time - time.sleep(1) + # 设置邮件通知系统 + mail.MAIL(args).mail() -sys_monitor.stop() -print("监听已停止...正在导出数据...") -sys_monitor.export(args.output) -print("结果输出到:", os.path.abspath(args.output))