解决 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 列表优化了解甚少。主要采用了,每个几百毫秒更新一次,减少一直跟新和添加来降低频次

  • 中文字符显示乱码

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

最好成果

相关推荐
得有个名14 分钟前
Windows 使用 Docker + WSL2 部署 Ollama(AMD 显卡推理)搭建手册‌
windows·docker·容器
SunshineBrother23 分钟前
Flutter性能优化细节
android·flutter·ios
摸鱼 特供版38 分钟前
一键无损放大视频,让老旧画面重焕新生!
windows·学习·音视频·软件需求
小周同学:2 小时前
Fiddler抓取App接口-Andriod/IOS配置方法
前端·ios·fiddler
车载操作系统---攻城狮3 小时前
[环境搭建篇] Windows 环境下如何安装repo工具
网络·windows·github
sukalot3 小时前
Windows 图形显示驱动开发-WDDM 3.2-本机 GPU 围栏对象(二)
windows·驱动开发
lindorx4 小时前
记一次误禁用USB导致键盘鼠标失灵的修复过程
windows·计算机外设·注册表·usb·hotpe
zimoyin4 小时前
解决 windows 11任务栏自动隐藏,窗口最大化后鼠标放到最下方任务栏不弹出了
windows·计算机外设
刘小哈哈哈9 小时前
iOS实现一个强大的本地状态记录容器
ios
青春不流名14 小时前
yarn application命令中各参数的详细解释
windows