解决 iOS日志在 Windows 电脑显示

背景及原因

由于公司新入职的测试工程师配置的电脑是 Windows电脑,在测试 iOS 相关需求的时候,无法看见打印的 NSLog,测试过程中带了很大的困难,要不 iOS 开发工程师,要把日志输出到界面,这样给整体体验以及开发人员带来了极大的不方便

使用 tidevice 或者 pymobiledevice3

这两个终端脚本功能基本很相似 tidevice 仅支持到 iOS16, pymobiledevice3 支持 iOS 17+。

在 Windows 电脑上安装之后,由于是终端界面,输出的日志看上不是特别的清晰 即使可以通过 grep 过滤对测试人员看的效果也不是很友好。

于是就想如果能够把终端获取到的日志转发出去在 Web页面展示,这样就能够定制 html 样式使其内容展示更加清晰,以及添加自己想要的功能

简述 tidevice 通信原理

tidevice 主要是基于 libimobiledevice 开源库和usbmux通信协议进行封装实现

  1. 设备连接
  • 发现设备:首先会通过 USB接口搜索链接到计算机上的 iOS 设备,利用libimobiledevice 实现设备的发现,在通过 USB协议与设备进行通信,请求连接识别到的设备

  • 建立链接:tidevice 会与设备建立一个安全通信通道。主要是有 lockdown 协议,该协议用于计算机和设备之间的身份认证。连接过程中或发送一些设备信息。例如:UDID、设备信号等

  1. 协议交互
  • 启动 **syslog_rela****y**服务 :在建立连接后,tidevice 会请求设备启动 syslog_relay 服务。syslog_relay 是 iOS 设备上的一个服务,负责收集和传输系统日志信息。tidevice 通过发送特定的命令到设备,告诉设备启动该服务。

  • 建立数据通道 :启动 syslog_relay 服务后,tidevice 会与该服务建立一个数据通道。通过这个通道,tidevice 可以从设备接收系统日志数据。数据通道的建立通常基于 TCP 或 UDP 协议,具体取决于 syslog_relay 服务的实现。

  1. 日志读取与解析
  • 读取日志数据 :一旦数据通道建立成功,tidevice 会开始从设备接收系统日志数据。这些数据是以二进制格式传输的,包含了设备的各种系统事件、应用程序日志等信息。

  • 解析日志数据tidevice 会对接收到的二进制日志数据进行解析,将其转换为可读的文本格式。解析过程通常涉及到对数据的解码、格式化等操作,以确保日志信息能够以清晰的方式显示在终端上。

日志转发到 Web页面

主要技术点: Python 3 + Web + websocket

Python3: 所需要的 module tidevice、asyncio(异步 I/O ) 、websockets (socket)

H5: 需要 css 、js 、 资源文件(font)

Python 作为 Server Web 作为 Client

Python服务端关键代码

python 复制代码
import asyncio
import json
import re
import websockets
# from pymobiledevice3.utils import try_decode

# 常量类
class Constants:
    Close = "Close"
    Start = "Start"
    SetFilters = "set_filters"  # 新增常量,表示设置过滤关键字的操作

process = None
filter_keys = []  # 新增变量,用于存储过滤关键字
read_lock = asyncio.Lock()  # 添加一个锁

match_regex = None

SYSLOG_LINE_SPLITTER = b'\n\x00'

async def data_generator():
    global process
    if not process:
        process = await asyncio.create_subprocess_exec(
            "tidevice",
            "syslog",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.DEVNULL,
        )
        print("tidevice syslog 命令已启动\n")

    buf = b''
    try:
        while True:
            async with read_lock:  # 使用锁确保同一时间只有一个协程可以读取数据
                output = await process.stdout.read(1024)
            if output == b"" and process.returncode is not None:
                break

            if len(output) == 0:
                raise ConnectionAbortedError()

            buf += output

            # SYSLOG_LINE_SPLITTER is used to split each syslog line
            if SYSLOG_LINE_SPLITTER in buf:
                lines = buf.split(SYSLOG_LINE_SPLITTER)

                # handle partial last lines
                if not buf.endswith(SYSLOG_LINE_SPLITTER):
                    buf = lines[-1]
                    lines = lines[:-1]

                for line in lines:
                    if len(line) == 0:
                        continue

                    match = None
                    is_valid = False
                    if not match_regex:
                        is_valid = True
                    else:    
                        for r in match_regex:
                            match = r.search(line.decode('utf-8'))
                            if  match:
                                is_valid = True
                                break
                    if is_valid:
                        print(f"字符串---- '{line}' 匹配成功。")
                        yield line.decode('utf-8')

    except KeyboardInterrupt:
        print("程序被手动中断,正在关闭...")
        if process:
            process.terminate()
            await process.wait()
        raise

async def websocket_handler(websocket):
    global filter_keys
    # 启动一个任务用于接收客户端消息
    receive_task = asyncio.create_task(receive_messages(websocket))
    try:
        async for data in data_generator():
            try:
                # 将过滤后的数据实时发送给客户端
                await websocket.send(data)
            except websockets.exceptions.ConnectionClosed:
                break
    except KeyboardInterrupt:
        pass
    finally:
        # 取消接收消息的任务
        receive_task.cancel()

async def receive_messages(websocket):
    global filter_keys , match_regex
    try:
        while True:
            message = await websocket.recv()
            receiveFromClientMsg(message)
            try:
                msg_data = json.loads(message)
                if msg_data.get("action") == Constants.SetFilters:
                    filter_keys = msg_data.get("filter_keys", [])
                    match_regex = [re.compile(f'.*({r}).*', re.DOTALL) for r in filter_keys]
                    print(f"收到过滤关键字: {filter_keys}")
            except json.JSONDecodeError:
                print("接收到的消息不是有效的 JSON 格式")
    except (websockets.exceptions.ConnectionClosed, KeyboardInterrupt):
        pass

def receiveFromClientMsg(msg):
    print(f"收到客户端消息---:{msg}")
    try:
        data = json.loads(msg)
        # 检查 "type" 键是否存在
        if "type" in data:
            msgType = data["type"]
            if msgType == Constants.Close:
                print(f"收到客户端消息:{msgType}")
        else:
            print("客户端消息中不包含 'type' 键")
    except json.JSONDecodeError:
        print("无法解析客户端发送的 JSON 数据")

async def main():
    try:
        async with websockets.serve(websocket_handler, "127.0.0.1", 5001):
            await asyncio.Future()
    except KeyboardInterrupt:
        print("程序被手动中断,正在关闭...")
        if process:
            process.terminate()
            await process.wait()

if __name__ == "__main__":
    asyncio.run(main())

Web 客户端关键代码

html 复制代码
<script>
        let socket;
        let labelTip = document.getElementById('socket-tip');
        let logArray = [];
        let updateTimer = null;
        let reconnectInterval;
        const RECONNECT_DELAY = 10000;
        let isConnected = false;
        let isManuallyClosed = false;
        let filterKeys = [];
        const searchBox = document.getElementById('searchBox');
        const tagContainer = document.getElementById('tag-container');

        function connectSocket() {
            const connectBtn = document.getElementById('connect-btn');
            socket = new WebSocket('ws://127.0.0.1:5001');

            socket.onopen = () => {
                labelTip.textContent = '已连接';
                labelTip.style.color = '#5FB878';
                connectBtn.classList.add('layui-btn-disabled');
                document.getElementById('unconnect-btn').style.display = 'inline-block';
                isConnected = true;
                isManuallyClosed = false;
                clearInterval(reconnectInterval);
            };

            socket.onmessage = (event) => {
                const data = event.data;
                logArray.push(data);
                updateList([data]);
            };

            socket.onerror = (error) => {
                labelTip.textContent = `连接错误: ${error}`;
                labelTip.style.color = '#FF5722';
            };

            socket.onclose = () => {
                labelTip.textContent = '连接已断开';
                labelTip.style.color = '#FF5722';
                connectBtn.classList.remove('layui-btn-disabled');
                document.getElementById('unconnect-btn').style.display = 'none';
                isConnected = false;

                if (!isManuallyClosed) {
                    labelTip.textContent = '尝试重新连接...';
                    reconnectInterval = setInterval(connectSocket, RECONNECT_DELAY);
                }
            };
        }

        function toggleConnection() {
            if (socket && socket.readyState === WebSocket.OPEN) {
                isManuallyClosed = true;
                socket.close();
            }
        }

        function sendFilterKeys() {
            if (!socket || socket.readyState!== WebSocket.OPEN) {
                console.error('WebSocket 未连接');
                return;
            }
            const message = JSON.stringify({ "action": "set_filters", "filter_keys": filterKeys });
            console.log(`发送的过滤关键字: ${message}`);
            socket.send(message);
            const filteredLogs = logArray.filter(log => shouldShowLog(log, filterKeys));
            const list = document.getElementById('myList');
            list.innerHTML = '';
            updateList(filteredLogs);
        }
    </script>

遇见问题及解决

  • 日志结构不完整

由于读取日志的时候是按照行读取的,如果有输入的日志都在同一行是没有问题的,有的时候咱们会打印 NSDictionary 这种数据结构的时候,里面的兼职对输出的时候会出现换行,这样就导致一个完整的日志会输出多行

  • 第一次解决

根据日志规则使用正则表达式匹配指定格式,然后手动拼接字符串

python 复制代码
async def data_generator():
    global process
    if not process:
        process = await asyncio.create_subprocess_exec(
            "tidevice",
            "syslog",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.DEVNULL,
        )
        print("tidevice syslog 命令已启动\n")

    buffer = ""
    pattern = r'\d{2}:\d{2}:\d{2}'
    log_pattern = re.compile(pattern)

    sendLogFlag = 0
    try:
        while True:
            async with read_lock:  # 使用锁确保同一时间只有一个协程可以读取数据
                output = await process.stdout.readline()
            if output == b"" and process.returncode is not None:
                break

            tmpLog = output.decode('utf-8')
            # 查找第一个时间戳的位置
            match_res = log_pattern.search(tmpLog)
            if match_res:
                sendLogFlag += 1
                if len(buffer) == 0:
                    buffer = tmpLog

                if sendLogFlag == 2:
                    sendLogFlag = 1
                    if len(buffer) > 0:
                        resLog = buffer
                        buffer = tmpLog
                        # 过滤逻辑
                        if filter_keys:  # 如果有过滤关键字
                            should_send = any(key in resLog for key in filter_keys)
                            if should_send:
                                yield resLog
                        else:  # 没有过滤关键字,正常发送
                            yield resLog
            else:
                buffer += tmpLog
    except KeyboardInterrupt:
        print("程序被手动中断,正在关闭...")
        if process:
            process.terminate()
            await process.wait()
        raise
  • 第二次解决

查看了一下 tidevice syslog 命令的源码方式解决,通过特定的分隔符进行分割,并且每次读取 1024 byte 数据内容

python 复制代码
SYSLOG_LINE_SPLITTER = b'\n\x00' #查看源码发现分隔符

async def data_generator():
    global process
    if not process:
        process = await asyncio.create_subprocess_exec(
            "tidevice",
            "syslog",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            stdin=asyncio.subprocess.DEVNULL,
        )
        print("tidevice syslog 命令已启动\n")

    buf = b''
    try:
        while True:
            async with read_lock:  # 使用锁确保同一时间只有一个协程可以读取数据
                output = await process.stdout.read(1024)
            if output == b"" and process.returncode is not None:
                break

            if len(output) == 0:
                raise ConnectionAbortedError()

            buf += output

            # SYSLOG_LINE_SPLITTER is used to split each syslog line
            if SYSLOG_LINE_SPLITTER in buf:
                lines = buf.split(SYSLOG_LINE_SPLITTER)

                # handle partial last lines
                if not buf.endswith(SYSLOG_LINE_SPLITTER):
                    buf = lines[-1]
                    lines = lines[:-1]

                for line in lines:
                    if len(line) == 0:
                        continue

                    match = None
                    is_valid = False
                    if not match_regex:
                        is_valid = True
                    else:    
                        for r in match_regex:
                            match = r.search(line.decode('utf-8'))
                            if  match:
                                is_valid = True
                                break
                    if is_valid:
                        print(f"字符串---- '{line}' 匹配成功。")
                        yield line.decode('utf-8')

    except KeyboardInterrupt:
        print("程序被手动中断,正在关闭...")
        if process:
            process.terminate()
            await process.wait()
        raise
  • 日志丢失内容

    内容丢失查看了一下 Mac 的控制台打印情况,发现有些时候控制台打印也有内容丢失的情况,这个问题就没有继续解决

  • 运行时间长 Web页面卡顿

    运行时间长卡顿,主要是由于累计过多的数据量,上千条数据是有的,会导致内存和页面滑动卡顿。 由于作者是 iOS 工程师,对 Web 列表优化了解甚少。主要采用了,每个几百毫秒更新一次,减少一直跟新和添加来降低频次

  • 中文字符显示乱码

    乱码问题还没有解决,广大读者有思路解决,希望留言 乱码问题还没有解决,广大读者有思路解决,希望留言 乱码问题还没有解决,广大读者有思路解决,希望留言

最好成果

相关推荐
i紸定i1 小时前
解决html-to-image在 ios 上dom里面的图片不显示出来
前端·ios·vue·html·html-to-image
cpsvps_net10 小时前
美国服务器环境下Windows容器工作负载智能弹性伸缩
windows
甄超锋11 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
cpsvps13 小时前
美国服务器环境下Windows容器工作负载基于指标的自动扩缩
windows
YungFan15 小时前
iOS26适配指南之UIButton
ios·swift
网硕互联的小客服16 小时前
Apache 如何支持SHTML(SSI)的配置方法
运维·服务器·网络·windows·php
etcix16 小时前
implement copy file content to clipboard on Windows
windows·stm32·单片机
许泽宇的技术分享17 小时前
Windows MCP.Net:基于.NET的Windows桌面自动化MCP服务器深度解析
windows·自动化·.net
非凡ghost18 小时前
AMS PhotoMaster:全方位提升你的照片编辑体验
windows·学习·信息可视化·软件需求
红橙Darren19 小时前
手写操作系统 - 编译链接与运行
android·ios·客户端