Jenkins设备监控(手机、手表)适配Windows、Linux

Jenkins设备监控系统教程

1. 设备监控的目的

在持续集成和自动化测试环境中,确保测试设备的可用性至关重要。设备监控的主要目的包括:

  1. 提高测试可靠性:及时发现离线或故障设备,避免无效测试运行
  2. 减少人工干预:自动化监控减少手动检查设备状态的需求
  3. 快速故障响应:第一时间发现问题并通知相关人员
  4. 资源优化利用:合理分配在线设备,提高测试效率
  5. 提升CI/CD流程稳定性:确保构建和测试任务在健康的设备上执行

2. 整体方案实现思路

2.1 系统架构

设备监控系统基于Jenkins构建,采用分布式架构:

复制代码
┌─────────────────┐    HTTP API    ┌──────────────────┐
│   中心节点      │◄───────────────┤   Jenkins Master │
└─────────┬───────┘                └──────────────────┘
          │                                
          │ 查询节点信息                 
          ▼                                
┌─────────────────┐    远程执行    ┌──────────────────┐
│   监控脚本      │───────────────►│   Jenkins Node1  │
└─────────┬───────┘                └─────────┬────────┘
          │                                  │ 检查设备状态
          │ 通知结果                         ▼
          ▼                          ┌──────────────────┐
┌─────────────────┐                 │ Android/iOS设备 │
│   钉钉通知      │                 └──────────────────┘
└─────────────────┘                 

2.2 核心组件

  1. Jenkins Master:提供API接口和任务调度
  2. Jenkins Nodes:实际连接设备的执行节点
  3. 监控脚本:负责检查各节点设备状态
  4. 通知系统:通过钉钉机器人发送警报

2.3 工作流程

  1. 脚本通过Jenkins API获取所有在线节点信息
  2. 解析各节点的环境变量,确定连接的设备
  3. 通过远程执行方式在各节点上检查设备状态
  4. 汇总检查结果,对离线设备发出警报

3. 脚本介绍

3.1 脚本功能概述

设备监控脚本(check_devices.py)具备以下核心功能:

  1. 节点发现:自动发现所有在线Jenkins节点
  2. 设备识别:解析节点环境变量识别连接的设备
  3. 状态检查:针对不同类型设备执行相应的检查命令
  4. 结果汇总:统计所有离线设备并生成报告
  5. 通知发送:通过钉钉机器人发送设备状态报告

3.2 支持的设备类型

  1. Android设备 :通过adb devices命令检查
  2. iOS设备 :通过idevice_id -l命令检查
  3. NG设备:通过Python脚本和SDS库检查
  4. Dilu设备:通过串口存在性检查

3.3 关键技术实现

3.3.1 Jenkins API交互

使用requests库与Jenkins API交互,获取节点信息:

python 复制代码
# 获取在线节点
api_url = f"{JENKINS_URL}/computer/api/json"
response = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN))
3.3.2 远程命令执行

通过Jenkins的script接口在远程节点执行命令:

python 复制代码
# 构造Groovy脚本在远程节点执行命令
groovy_script = f"""
    def sout = new StringBuffer(), serr = new StringBuffer()
    def proc = ['sh', '-c', '{command}'].execute()
    proc.consumeProcessOutput(sout, serr)
    proc.waitForOrKill(30000)
    println "STDOUT: " + sout.toString()
    println "STDERR: " + serr.toString()
    println "EXIT_CODE: " + proc.exitValue()
"""
3.3.3 跨平台兼容

根据不同操作系统类型选择合适的命令执行方式:

python 复制代码
if is_windows:
    # Windows系统使用批处理命令
    groovy_script = f"""
        def sout = new StringBuffer(), serr = new StringBuffer()
        def proc = ['cmd', '/c', '{command}'].execute()
        // ...
    """
else:
    # Unix-like系统使用shell命令
    groovy_script = f"""
        def sout = new StringBuffer(), serr = new StringBuffer()
        def proc = ['sh', '-c', '{command}'].execute()
        // ...
    """

3.4 设备检查实现

python 复制代码
import requests
import re
from requests.auth import HTTPBasicAuth

# Jenkins配置
JENKINS_URL = "http://"
JENKINS_USER = "admin"
JENKINS_TOKEN = "admin123" # 密码即可
node_env_vars_type = ["android_devices_id", "ios_devices_id", "ng_devices_id", "dilu_port"]
# node_env_vars_type = ["ng_devices_id"]
# 获取Jenkins CSRF保护所需的crumb
def get_crumb(session):
    crumb_url = f"{JENKINS_URL}/crumbIssuer/api/json"
    try:
        response = session.get(crumb_url, timeout=10)
        if response.status_code == 200:
            crumb_data = response.json()
            return crumb_data["crumbRequestField"], crumb_data["crumb"]
        else:
            print(f"⚠️ 获取crumb失败,状态码:{response.status_code}")
            return "", ""
    except Exception as e:
        print(f"⚠️ 获取crumb异常:{str(e)[:50]}")
        return "", ""

# 在远程节点上执行命令
def execute_remote_command(node_name, command, is_windows=False):
    # 创建会话并设置认证信息
    session = requests.Session()
    session.auth = HTTPBasicAuth(JENKINS_USER, JENKINS_TOKEN)
    
    # 获取CSRF Token
    crumb_header, crumb_value = get_crumb(session)
    
    # 设置请求头部
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    if crumb_header and crumb_value:
        headers[crumb_header] = crumb_value
    
    # 构造远程执行命令的URL
    exec_url = f"{JENKINS_URL}/computer/{node_name}/scriptText"
    
    # 根据操作系统类型选择执行方式
    if is_windows:
        # Windows系统使用批处理命令
        groovy_script = f"""
            def sout = new StringBuffer(), serr = new StringBuffer()
            def proc = ['cmd', '/c', '{command}'].execute()
            proc.consumeProcessOutput(sout, serr)
            proc.waitForOrKill(30000)
            println "STDOUT: " + sout.toString()
            println "STDERR: " + serr.toString()
            println "EXIT_CODE: " + proc.exitValue()
        """
    else:
        # Unix-like系统(macOS/Linux)使用shell命令
        groovy_script = f"""
            def sout = new StringBuffer(), serr = new StringBuffer()
            def proc = ['sh', '-c', '{command}'].execute()
            proc.consumeProcessOutput(sout, serr)
            proc.waitForOrKill(30000)
            println "STDOUT: " + sout.toString()
            println "STDERR: " + serr.toString()
            println "EXIT_CODE: " + proc.exitValue()
        """
    
    data = {
        "script": groovy_script
    }
    
    try:
        response = session.post(
            exec_url,
            headers=headers,
            data=data,
            timeout=30
        )
        response.raise_for_status()
        return response.text
    except Exception as e:
        print(f"❌ 节点[{node_name}]执行命令失败:{command} → 错误:{str(e)[:50]}")
        return f"ERROR: {str(e)[:50]}"

# 获取所有在线节点
def get_online_nodes():
    # 创建 Jenkins API 请求
    api_url = f"{JENKINS_URL}/computer/api/json"
    # 使用 Token 进行认证
    response = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN))
    if response.status_code != 200:
        raise Exception(f"无法连接到 Jenkins: {response.status_code} - {response.text}")
    
    data = response.json()
    online_nodes = []
    
    # 遍历在线节点
    for computer in data.get("computer", []):
        # 跳过离线节点
        if computer.get("offline", True):
            continue
            
        node_name = computer.get("displayName")
        # 获取节点详细配置
        config_url = f"{JENKINS_URL}/computer/{node_name}/config.xml"
        config_response = requests.get(config_url, auth=(JENKINS_USER, JENKINS_TOKEN))
        
        if config_response.status_code != 200:
            continue
            
        config_xml = config_response.text
        
        # 解析节点标签
        node_labels = []
        label_match = re.search(r"<label>(.*?)</label>", config_xml, re.DOTALL)
        if label_match:
            node_labels = label_match.group(1).strip().split()
        
        # 解析节点环境变量 (根据你提供的XML结构调整解析方法)
        env_vars = {}
        if "<envVars" in config_xml and "</envVars>" in config_xml:
            # 查找环境变量部分
            env_vars_section = config_xml.split("<envVars")[1].split("</envVars>")[0]
            # 使用正则表达式查找所有的键值对
            # 在你提供的XML中,键值对是这样组织的:
            # <string>android_devices_id</string>
            # <string>10CF4809DD002B4</string>
            strings = re.findall(r"<string>(.*?)</string>", env_vars_section)
            # 成对处理这些字符串作为键值对
            for i in range(0, len(strings), 2):
                if i + 1 < len(strings):
                    key = strings[i].strip().lower()
                    value = strings[i+1].strip()
                    env_vars[key] = value
        
        # 判断节点环境变量是否有node_env_vars_type里的值,如果没有则跳过
        has_required_env_var = any(env_var in env_vars for env_var in node_env_vars_type)
        if not has_required_env_var:
            continue
            
        # 判断节点是否为Windows系统(通过节点名称判断)
        if "mac" in node_name:
            is_windows = False
        else:
            is_windows = True
            
        # 如果有,则获取节点环境变量值并判断是否在线
        node_info = {
            "name": node_name,
            "labels": node_labels,
            "env_vars": env_vars,
            "is_windows": is_windows
        }
        online_nodes.append(node_info)
        
    return online_nodes

def check_devices(node_name, devices_type, devices_id, is_windows=False):
    if devices_type=="android_devices_id":
        # 使用adb命令检查设备是否在线
        try:
            command = f"adb devices"
            result = execute_remote_command(node_name, command, is_windows)
            if devices_id in result:
                return True
            else:
                return False
        except Exception:
            return False
            
    if devices_type=="ios_devices_id":
        # 使用idevice_id命令检查iOS设备是否在线
        try:
            if is_windows:
                # Windows上不支持iOS设备
                command = f"echo iOS devices not supported on Windows"
                result = execute_remote_command(node_name, command, is_windows)
                return False
            else:
                # 在Unix-like系统上使用idevice_id检查iOS设备
                # 使用完整路径或者source profile来确保环境变量正确
                command = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; which idevice_id || echo \"idevice_id not found\""
                check_result = execute_remote_command(node_name, command, is_windows)
                
                if "idevice_id not found" in check_result:
                    # 尝试使用常见路径
                    command = f"/usr/local/bin/idevice_id -l 2>/dev/null | grep \"{devices_id}\" || /opt/homebrew/bin/idevice_id -l 2>/dev/null | grep \"{devices_id}\" || echo \"Device not found\""
                    result = execute_remote_command(node_name, command, is_windows)
                    return devices_id in result and "Device not found" not in result
                else:
                    # 如果which找到了idevice_id,则正常使用
                    command = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; idevice_id -l | grep \"{devices_id}\" || echo \"Device not found\""
                    result = execute_remote_command(node_name, command, is_windows)
                    return devices_id in result and "Device not found" not in result
        except Exception as e:
            print(f"    iOS设备检查异常: {str(e)}")
            return False
            
    if devices_type=="ng_devices_id":
        # NG设备检查需要使用Python脚本来检查
        try:

            sds_file_name = "SDSApplicationServer"
            if is_windows:
                sds_file_name = sds_file_name + ".exe"
                # 根据进程名称杀死SDS然后等待10s
                # 使用webtool命令查找ng_devices
                kill_cmd = f"taskkill /F /IM {sds_file_name} >nul 2>&1 & timeout /t 10 >nul"
                result1 = execute_remote_command(node_name, kill_cmd, is_windows)
                # 执行wbtool info命令,需要先确保环境变量正确
                wbtool_cmd = f"where wbtool >nul 2>&1 && wbtool info || echo \"wbtool not found\""
                result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
                if devices_id in result2 and "Device not found" not in result2:
                    return True
                else:
                    return False
            else:
                # mac逻辑
                kill_cmd = f"pkill -f {sds_file_name} >/dev/null 2>&1; sleep 10"
                result1 = execute_remote_command(node_name, kill_cmd, is_windows)
                # 执行wbtool info命令,需要先确保环境变量正确
                wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; which wbtool || echo \"wbtool not found\""
                check_result = execute_remote_command(node_name, wbtool_cmd, is_windows)
                
                if "wbtool not found" in check_result:
                    # 尝试使用常见路径
                    wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; /usr/local/bin/wbtool info 2>/dev/null | grep \"{devices_id}\" || /opt/homebrew/bin/wbtool info 2>/dev/null | grep \"{devices_id}\" || echo \"Device not found\""
                    result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
                else:
                    # 如果which找到了wbtool,则正常使用
                    wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; wbtool info | grep \"{devices_id}\" || echo \"Device not found\""
                    result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
                
                if devices_id in result2 and "Device not found" not in result2:
                    return True
                else:
                    return False
        except Exception as e:
            print(f"    NG设备检查异常: {str(e)}")
            return False
            
    if devices_type=="dilu_port":
        try:
            # 检查串口是否存在
            if is_windows:  # Windows
                command = f"mode"
                result = execute_remote_command(node_name, command, is_windows)
                if devices_id.upper() in result.upper():
                    return True
            else:  # Unix-like systems
                command = f"ls /dev/{devices_id}"
                result = execute_remote_command(node_name, command, is_windows)
                # 如果命令成功执行且没有错误信息,则设备存在
                return "No such file" not in result and "ERROR" not in result
            return False
        except Exception:
            return False

# 发送报告
def send_report(offline_devices):
    if not offline_devices:
        print("所有设备均在线")
        return
        
    # 构建报告内容
    title = "【Device Check Report】"
    text = "以下设备不在线:\n"
    for device in offline_devices:
        text += f"- 节点名称: {device['node_name']}, 设备类型: {device['device_type']}, 设备ID: {device['device_id']}\n"
    
    print(text)
    # 发送钉钉通知
    try:
        # 钉钉配置机器人获取
        webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=666'
        
        # 构建钉钉消息格式
        message = {
            "msgtype": "markdown",
            "markdown": {
                "title": title,
                "text": text
            },
            "at": {
                "atMobiles": [],
                "isAtAll": False
            }
        }
        
        # 发送POST请求
        response = requests.post(webhook_url, json=message)
        
        if response.status_code == 200:
            print("钉钉通知发送成功")
        else:
            print(f"钉钉通知发送失败,状态码: {response.status_code}")
    except Exception as e:
        print(f"发送钉钉通知时发生异常: {e}")
    
    
def main():
    """
    主函数,执行设备检查流程
    """
    offline_devices = []
    
    # 获取所有在线节点
    try:
        nodes = get_online_nodes()
        print(f"发现 {len(nodes)} 个在线节点")
    except Exception as e:
        print(f"获取节点信息失败: {e}")
        return
    
    # 遍历每个节点,检查其设备状态
    for node in nodes:
        node_name = node["name"]
        env_vars = node["env_vars"]
        is_windows = node["is_windows"]
        
        print(f"检查节点: {node_name}")
        
        # 检查每种类型的设备
        for device_type in node_env_vars_type:
            if device_type in env_vars:
                device_id = env_vars[device_type]
                print(f"  检查设备类型: {device_type}, ID: {device_id}")
                
                is_online = check_devices(node_name, device_type, device_id, is_windows)
                if not is_online:
                    offline_devices.append({
                        "node_name": node_name,
                        "device_type": device_type,
                        "device_id": device_id
                    })
                    print(f"    设备不在线!")
                else:
                    print(f"    设备在线")
    
    # 发送报告
    send_report(offline_devices)

if __name__ == "__main__":
    main()

4. Jenkins定时触发配置

4.1 创建定时任务

  1. 登录Jenkins管理界面
  2. 点击"新建任务"
  3. 输入任务名称,例如"Device-Monitor"
  4. 选择"构建一个自由风格的软件项目"
  5. 点击"确定"

4.2 配置定时触发

在任务配置页面:

  1. 找到"构建触发器"部分
  2. 勾选"定时构建"
  3. 在日程表中输入定时表达式:

常用表达式示例:

  • H/30 * * * *:每30分钟执行一次
  • H 8 * * *:每天早上8点执行一次
  • H 8 * * 1-5:周一至周五每天早上8点执行一次
  • H H * * *:每天随机时间执行一次

4.3 配置构建步骤

  1. 在"构建"部分点击"增加构建步骤"
  2. 选择"执行Windows批处理命令"或"Execute shell"(根据主节点系统类型)
  3. 输入执行脚本的命令:
bash 复制代码
python D:\project\SuuntoTest\jenkins\check_devices.py

4.4 配置环境变量

在"常规"部分勾选"参数化构建过程",添加以下参数:

  • JENKINS_URL: Jenkins服务器地址
  • JENKINS_USER: Jenkins用户名
  • JENKINS_TOKEN: Jenkins API token

或者在"构建环境"中配置相关环境变量。

相关推荐
Irene19911 小时前
Windows环境下使用Bash命令的解决方案和命令行工具推荐(附:前端开发者 Windows 终端配置清单)
windows·工具推荐·bash命令
FreeBuf_1 小时前
微软悄然修复潜伏8年的Windows LNK漏洞
windows·microsoft
skywalk81631 小时前
winget 是微软官方推出的 Windows 包管理器(Windows Package Manager)
windows·microsoft
阿桂有点桂10 小时前
C#使用VS软件打包msi安装包
windows·vscode·c#
2501_9151063211 小时前
如何查看手机使用记录:Android和iOS设备全面指南
android·ios·智能手机·小程序·uni-app·iphone·webview
a21558332011 小时前
Oracle 11g ADG 主从复制配置手册(Windows 环境)
windows·主从同步·oracle11g
Digitally12 小时前
如何完全从Itel手机SIM卡中删除联系人
智能手机
nee~13 小时前
Android设备USB连接转无线操作(windows)
android·windows
wanhengidc14 小时前
使用云手机都要注意哪些?
运维·服务器·科技·游戏·智能手机