需求背景:
题主所属制造业,有大量打印标签的需求,当时在做MES标签管理时,公司内部购买了外部智高软件的API,通过调用该打印API,去按照设计模版打出标签张贴。
但是该服务在运行过程中发现一个常规性问题:长时间运行一段时间后,智高打印API服务会抛错“句柄无效”,导致后续所有的打印队列无法进行。
对于制造业来说,不分昼夜的在生产,白天还OK,可以人为任务重启获取句柄,但晚上很可能联系不上基地运维IT,会导致货物堆积,进而影响入库和发货。(员工已经高度依赖系统打印,不愿,QA也不准人为用EXCEL打印)。
综上,业务和运维老大要求我们组解决这个问题,但开发同事认为这是供应商的问题,供应商认为是打印机驱动的问题,一直没有真正的去解决这个问题。
解决思路:
导致句柄无效的根本原因其实并没有找到,查了下很多原因会导致windows这个问题,但这里我想可以做一个异常处理,保证代码的健壮性。
MES打印主程序做一个异常处理,获取句柄无效异常时,调用本地的另一个服务操作打印驱动程序重获句柄,操作本地系统和打印服务释放系统资源重新获取句柄。
代码实施:
本次选择python-flask提供接口服务,subprocess启动应用,pywinauto控制应用进行启动操作。
项目目录:
restartAPI
-main.py #主程序
-config.json #配置文件
-log
--log.txt #日志文件
1.必要包导入
import logging #日志打印
from logging.handlers import RotatingFileHandler #日志打印
from flask import Flask, request,jsonify #使用falsk框架对外提供接口服务
import os #操作系统,执行命令行
import time #引入等待应用打开时间,避免异常
import subprocess #引入创建进程
import json # 配置文件用Json写的
from pywinauto import Application # 操作打印api窗口控件
2.实例化flask-app,挂上日志
公司的开发往往之前不爱加日志,导致异常的时候排错很麻烦,所以我认为加日志的处理比较有必要。
app = Flask(__name__)
logging.basicConfig(level=logging.INFO) # !!!默认警告(warning)级别,需要重写默认报错级别
log_handler = RotatingFileHandler('./log/app.log', maxBytes=100000000, backupCount=6, encoding='utf-8') # 最大100M,6个文件,utf-8编码
log_handler.setLevel(logging.INFO) # 设置日志级别,但并没有卵用
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
log_handler.setFormatter(formatter)
app.logger.addHandler(log_handler)
问题点:即使log_handler.setLevel(logging.INFO),但是实际打印INFO基本还是会被过滤掉,原因是这里的设置只针对RotatingFileHandler处理器,但实际给他的是经过flask过滤的,所以必须重写flask根日志设置级别。
3.控制重启应用获取句柄函数
def StartAPI():
windowsAPP = Application().connect(title='CCPrintingAPI') #控制连接到打印api服务(此服务已经打包成EXE文件)
main_window = windowsAPP.window(title='CCPrintingAPI')
# 确保窗口可见并启用
main_window.wait('visible')
main_window.wait('enabled')
# 该服务并非启动即用,还需要控制点击其启动按钮,如果禁用则已经启用
button = main_window.child_window(title='Start', control_type='System.Windows.Forms.Button')
if button.is_enabled():
button.click()
else:
print("已经自动启动")
注意:读取窗口需要管理员权限,要以管理员打开IDE去运行代码,否则报错喔
4.对外路由函数
@app.route("/errorProcess", methods=["get"])
def root():
# 获取操作方
uuid = request.args.get("UUID") #加入授权机制,记录调用方
client_ip = request.remote_addr #获取远程调用IP地址
app.logger.info(f"接口被IP地址:{client_ip}调用")
# 有授权才能执行,否则跑错
if uuid:
with open('config.json', 'r') as file: # 加载配置文件
config = json.load(file)
exe_path = config['Program']['exe_path']
user_uuids = config['Program']['USER_UUIDS']
if uuid in user_uuids:
# 异常捕错
try:
os.system(f"taskkill /f /im {os.path.basename(exe_path)}") #执行中止API进程
time.sleep(1) # 给系统一秒宽容时间
subprocess.Popen(exe_path) # 启动进程
time.sleep(3) # 给系统3秒宽容时间,对执行时间4秒现场可以等待,否则还没打开应用,调用下面重启API方法会找不到窗口。
StartAPI()
app.logger.info(f"授权用户:{user_uuids[uuid]},在{client_ip}执行重启成功")
return jsonify({"code": 200, "msg": "API已经重启"})
except Exception as e:
app.logger.error(f"{user_uuids[uuid]},在{client_ip}执行重启失败,Error restarting the API: {e}")
return jsonify({"code": 500, "msg": "API重启失败,请后台看日志"}), 500
else:
app.logger.warning(f"用户在{client_ip},携带异常uuid:{uuid}调用")
return jsonify({"code": 400, "msg": "请携带正确的UUID参数"}), 400
else:
app.logger.warning(f"用户在{client_ip},没有携带参数调用")
return jsonify({"code": 400, "msg": "请携带UUID参数,如有需要请联系管理员"}), 400
5.脚本启动:
if __name__ == "__main__":
with open('config.json', 'r') as file1:
config = json.load(file1)
ip = config['Program']['IP'] #读取IP配置,本机回环地址不能被客户端请求
app.run(debug=False, port=5000, host=ip)
注意:部署正式环境的时候要用局域网地址,不能用回环地址,回环地址有隔离性。
5.编写配置文件
json格式,后续从数据库读取基本配置,保证各子基地的可维护性。
{
"Program": {
"exe_path": "启动的api路径,注意转义,/要用//",
"IP": "运行服务器IP地址",
"USER_UUIDS": {
"自己加密钥":"用户名" ,
"自己加密钥":"用户名"
}
}
}
6.打包成EXE部署服务器
子基地的服务器是java环境,是Windows服务器;同时是基地IT人员管理,简单快速的启用无疑是打包成EXE了,这里在程序目录用pyinstaller打包就可以了。
pyinstaller --onefile main.py
OVER,收工,把接口文档给开发同事就可以了。