• 分类

  • 重置

如何使用Swift来实现一个命令行工具的方法

    本文即简单介绍了如何在Swift中开发命令行工具,以及与Shell命令的交互。水文一篇,不喜勿喷。

    主要是使用该工具来解析微信的性能监控组件Matrix的OOM Log。

    基本模块

    这里,仅简单介绍了常见的基本模块。

    Process

    Process类可以用来打开另外一个子进程,并监控其运行情况。

    1. launchPath:指定了执行路径。如可以设置为 /usr/bin/env ,这个命令可以用于打印本机上所有的环境变量;也可以用于执行shell命令,如果你接了参数的话。本文的Demo就用它来执行输入的命令。
    2. arguments:参数,以数组形式传递即可。
    3. launch:调用launch函数即可启动process,用于执行命令。
    4. waitUntilExit:一般执行Shell命令,需要等待命令返回。
    5. terminationStatus:当前process的结束状态,正常为0.
    6. standardOutput:standardOutput对应于终端的标准输出。standardError则是错误输出。

    Pipe

    Pipe这个类就是操作系统的管道,在这里用来接受子进程的输出。这里,可以用于将process的输出传递至管道指定的地方,如一个output变量,或者文件也可以。

    • fileHandleForReading:pipe从哪里读取内容?
    • fileHandleForWriting:pipe将内容写到哪里?

    CommandLine

    用于获取脚本参数而已。

    
    print(CommandLine.argc) // 2
    print(CommandLine.arguments) // ["./test.swift", "hello"]

    封装Shell命令

    仅执行Shell命令

    这里提供了两种调用Shell命令的封装函数,个人更倾向于第二种,直接将Shell命令及参数封装成一个字符串传入即可。

    
    @discardableResult
    func runShell(_ command: String) -> Int32 {
     let task = Process()
     task.launchPath = "/bin/bash"
     task.arguments = ["-c", command]
     task.launch()
     task.waitUntilExit()
     return task.terminationStatus
    }
    
    @discardableResult
    func runShellWithArgs(_ args: String...) -> Int32 {
     let task = Process()
     task.launchPath = "/usr/bin/env"
     task.arguments = args
     task.launch()
     task.waitUntilExit()
     return task.terminationStatus
    }
    
    

    使用如下:

    
    runShell("pwd")
    runShell("ls -l")
    
    runShellWithArgs("pwd")
    runShellWithArgs("ls", "-l")
    
    

    需要Shell命令的输出内容

    这里就需要使用到Pipe了。

    
    @discardableResult
    func runShellAndOutput(_ command: String) -> (Int32, String?) {
     let task = Process()
     task.launchPath = "/bin/bash"
     task.arguments = ["-c", command]
     
     let pipe = Pipe()
     task.standardOutput = pipe
     task.standardError = pipe
     
     task.launch()
     
     let data = pipe.fileHandleForReading.readDataToEndOfFile()
     let output = String(data: data, encoding: .utf8)
     
     task.waitUntilExit()
     
     return (task.terminationStatus, output)
    }
    
    @discardableResult
    func runShellWithArgsAndOutput(_ args: String...) -> (Int32, String?) {
     let task = Process()
    
     task.launchPath = "/usr/bin/env"
     task.arguments = args
     
     let pipe = Pipe()
     task.standardOutput = pipe
     task.standardError = pipe
     
     task.launch()
     
     let data = pipe.fileHandleForReading.readDataToEndOfFile()
     let output = String(data: data, encoding: .utf8)
     
     task.waitUntilExit()
     
     return (task.terminationStatus, output)
    }

    使用如下:

    
    let (ret1, output1) = runShellAndOutput("ls -l")
    if let output11 = output1 {
     print(output11)
    }
    
    let (ret2, output2) = runShellWithArgsAndOutput("ls", "-l")
    if let output22 = output2 {
     print(output2)
    }
    
    

    如何解析Matrix的OOM Log

    Matrix的OOM Log格式如下,其实就是一个大JSON:

    
    {
     "head": {
      "protocol_ver": 1,
      "phone": "iPhone10,1",
      "os_ver": "13.4",
      "launch_time": 1589361495000,
      "report_time": 1589362109100,
      "app_uuid": ""
     },
     "items": [
      {
       "tag": "iOS_MemStat",
       "info": "",
       "scene": "",
       "name": "Malloc 12.54 MiB",
       "size": 146313216,
       "count": 1,
       "stacks": [
        {
         "caller": "f07199ac8a903127b17f0a906ffb0237@84128",
         "size": 146313216,
         "count": 1,
         "frames": [
          {
           "uuid": "a0a7d67af0f3399a8f006f92716d8e6f",
           "offset": 67308
          },
          {
           "uuid": "a0a7d67af0f3399a8f006f92716d8e6f",
           "offset": 69836
          },
          {
           "uuid": "f07199ac8a903127b17f0a906ffb0237",
           "offset": 84128
          },
          {
           "uuid": "b80198f7beb93e79b25c7a27d68bb489",
           "offset": 14934312
          },
          {
           "uuid": "1a46239df2fc34b695bc9f38869f0c85",
           "offset": 1126304
          },
          {
           "uuid": "1a46239df2fc34b695bc9f38869f0c85",
           "offset": 123584
          },
          {
           "uuid": "1a46239df2fc34b695bc9f38869f0c85",
           "offset": 1135100
          }]
        }
       ]
      }
     ]
    }
    

    解析的思路其实非常简单,将JSON转为Model,然后根据所需,提取对应的信息即可。

    uuid是mach-o的唯一标识,offset则是符号相对于mach-o基地址的偏移量。拿到dSYM文件,使用 atos 命令即可进行符号化。

    
    guard let rawLogModel = MatrixOOMLogParser.parse() else { exit(-1) }
    print("______ Start to process Matrix OOM Log ...")
    
    let group = DispatchGroup()
    
    var metaLog = ""
    
    for item in bodyInfo.items {
     guard let stacks = item.stacks else { continue }
     
     group.enter()
     
     DispatchQueue.global().async {
      var log = "______ item ______ name: \(item.name), size: \(item.size), count: \(item.count) \n"
      metaLog += log
      
      for stack in stacks {
       let outputs = stack.frames.map({ (frame: MatrixOOMLogModelFrame) -> String in
        // let uuid = frame.uuid
        let offset = frame.offset
        let instructionAddress = loadAddress + offset
        let (_, output) = runShellAndOutput("xcrun atos -o \(dwarf) -arch arm64 -l 0x1 \(instructionAddress.hexValue)")
        return output ?? ""
       })
       
       log += outputs.joined()
       
       print(log)
      }
      
      group.leave()
     }
    }
    
    group.wait()
    
    print("\n\(metaLog)\n")
    
    print("______ Finished processing Matrix OOM Log ...")

    MatrixOOMLogParser.parse() 就是将JSON转为Model,这里用的就是Swift里边的Codable。

    这里有一个需要注意的点,Mac CLI没有Bundle的概念,只有一个bin文件。所以对于原始的JSON文件,只能通过外部bundle的方式来添加。通过 New->Target 单独建立一个bundle。需要在 Xcode -> Build Phases -> Copy Files 中添加该bundle名,然后即可通过 Bundle(url: mockDataBundleURL) 来加载该bundle并获取其中的log文件了。

    因为atos的执行时间较长,所以大量的符号化操作会非常耗时。一般来说,这段代码执行六七分钟左右,可以将一个Matrix的OOM Log完全符号化。而符号化之后的记录如何分析,就是另外一个话题了。

    参考资料

    How do I run an terminal command in a swift script? (e.g. xcodebuild)

    到此这篇关于如何使用Swift来实现一个命令行工具的文章就介绍到这了,更多相关Swift 命令行内容请搜索lingkb以前的文章或继续浏览下面的相关文章希望大家以后多多支持lingkb!