You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift5 macOS命令行工具中URLSession.dataTask不执行如何解决

问题根因

你遇到的问题完全是命令行工具和GUI应用/Playground的运行环境差异导致的,没有什么玄学配置问题:

  • 异步回调不执行:命令行工具执行完主线程所有同步代码后会直接退出进程,不会像常规GUI应用那样自动维护运行循环等待异步任务回调。你调用resume()发起网络请求后,主线程立刻走到代码末尾结束进程,URLSession的回调根本没机会执行,所以闭包里的4.x系列打印完全不触发。Playground会自动保持运行等待异步任务完成,所以同逻辑代码在Playground里能正常跑。
  • 开启沙盒触发崩溃:你导入AppKit用NSAttributedString做HTML解析的逻辑,底层依赖NSApplication实例和WebKit渲染框架,命令行工具默认不会初始化AppKit应用环境,沙盒下也没有WebKit渲染需要的权限,所以会触发EXC_BAD_INSTRUCTION崩溃,和网络出站权限本身无关。
修复方案

不需要做任何特殊的工程配置,按以下几点改代码即可:

  1. 完全移除AppKit依赖,删掉HTML解析相关逻辑:joker的checkip接口返回的是Your IP is x.x.x.x格式的纯文本,直接做字符串截取就能拿到IP,不需要用重量级的HTML解析能力,从根源避免AppKit相关崩溃。
  2. 用信号量控制进程生命周期,等所有网络请求执行完成后再退出,不要让进程提前结束。
  3. 网络请求参数不要手动拼接字符串,用URLComponents和URLQueryItem处理,避免特殊字符转义问题。
  4. 后续配置launch agent时,不要给工具开沙盒权限,设置定时触发即可,不需要让工具本身常驻后台。
修正后的可运行代码
import Foundation

// 基础配置
let userName = "yourUserName"
let password = "yourPassword"
let domain = "yourDomainName"
let needUpdateSubdomains = ["", "ftp", "www"] // 空字符串对应主域名

guard let checkIPURL = URL(string: "https://svc.joker.com/nic/checkip") else {
    print("公网IP检查接口配置无效")
    exit(EXIT_FAILURE)
}

var currentIP = ""
// 信号量用来阻塞主线程,等待异步任务完成
let taskSemaphore = DispatchSemaphore(value: 0)

// 第一步:获取当前公网IP
var ipCheckRequest = URLRequest(url: checkIPURL)
ipCheckRequest.httpMethod = "GET" // 该接口不需要POST请求
URLSession.shared.dataTask(with: ipCheckRequest) { data, response, error in
    defer { taskSemaphore.signal() } // 无论请求成功失败,最后都释放信号量
    
    if let reqError = error {
        print("获取公网IP失败:\(reqError.localizedDescription)")
        return
    }
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200,
          let respData = data,
          let respText = String(data: respData, encoding: .utf8)?
        .trimmingCharacters(in: .whitespacesAndNewlines) else {
        print("公网IP接口返回异常")
        return
    }
    // 从返回文本里截取IP:接口返回格式固定为"Your IP is x.x.x.x"
    guard let lastSpaceIndex = respText.lastIndex(of: " ") else {
        print("IP格式解析失败,返回内容:\(respText)")
        return
    }
    currentIP = String(respText[respText.index(after: lastSpaceIndex)...])
    print("当前公网IP:\(currentIP)")
    
    // 第二步:批量更新所有域名的解析记录
    let updateGroup = DispatchGroup()
    for sub in needUpdateSubdomains {
        let host = sub.isEmpty ? domain : "\(sub).\(domain)"
        guard var updateComponents = URLComponents(string: "https://svc.joker.com/nic/update") else { continue }
        updateComponents.queryItems = [
            URLQueryItem(name: "username", value: userName),
            URLQueryItem(name: "password", value: password),
            URLQueryItem(name: "hostname", value: host),
            URLQueryItem(name: "myip", value: currentIP)
        ]
        guard let updateURL = updateComponents.url else { continue }
        
        updateGroup.enter()
        var updateRequest = URLRequest(url: updateURL)
        updateRequest.httpMethod = "GET"
        URLSession.shared.dataTask(with: updateRequest) { _, updateResp, updateErr in
            defer { updateGroup.leave() }
            if let updateErr = updateErr {
                print("更新域名\(host)失败:\(updateErr.localizedDescription)")
                return
            }
            guard let updateHttpResp = updateResp as? HTTPURLResponse,
                  updateHttpResp.statusCode == 200 else {
                print("更新域名\(host)接口返回异常")
                return
            }
            print("域名\(host)解析更新成功")
        }.resume()
    }
    // 等待所有域名更新完成
    updateGroup.wait()
    print("所有域名解析更新完成,当前IP:\(currentIP)")
}.resume()

// 阻塞主线程,等待整个流程执行完成
taskSemaphore.wait()
exit(EXIT_SUCCESS)
launch agent配置注意点
  • plist里的ProgramArguments字段必须填编译后二进制文件的绝对路径
  • 不需要配置任何沙盒相关权限
  • 建议设置StartInterval为300(每5分钟执行一次),每次执行完更新逻辑工具会自动退出,不需要常驻后台,资源占用极低

内容的提问来源于stack exchange,提问作者SouthernYankee65

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 14:21:21