Swift5 macOS命令行工具中URLSession.dataTask不执行如何解决
问题根因
你遇到的问题完全是命令行工具和GUI应用/Playground的运行环境差异导致的,没有什么玄学配置问题:
- 异步回调不执行:命令行工具执行完主线程所有同步代码后会直接退出进程,不会像常规GUI应用那样自动维护运行循环等待异步任务回调。你调用
resume()发起网络请求后,主线程立刻走到代码末尾结束进程,URLSession的回调根本没机会执行,所以闭包里的4.x系列打印完全不触发。Playground会自动保持运行等待异步任务完成,所以同逻辑代码在Playground里能正常跑。 - 开启沙盒触发崩溃:你导入
AppKit用NSAttributedString做HTML解析的逻辑,底层依赖NSApplication实例和WebKit渲染框架,命令行工具默认不会初始化AppKit应用环境,沙盒下也没有WebKit渲染需要的权限,所以会触发EXC_BAD_INSTRUCTION崩溃,和网络出站权限本身无关。
修复方案
不需要做任何特殊的工程配置,按以下几点改代码即可:
- 完全移除
AppKit依赖,删掉HTML解析相关逻辑:joker的checkip接口返回的是Your IP is x.x.x.x格式的纯文本,直接做字符串截取就能拿到IP,不需要用重量级的HTML解析能力,从根源避免AppKit相关崩溃。 - 用信号量控制进程生命周期,等所有网络请求执行完成后再退出,不要让进程提前结束。
- 网络请求参数不要手动拼接字符串,用
URLComponents和URLQueryItem处理,避免特殊字符转义问题。 - 后续配置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
相关产品推荐
相关产品推荐

