1
2
3
errSecInternalComponent
Timed out while enabling automation mode
The test runner hung before establishing connection

如果你在自建的 macOS GitHub Actions Runner 上遇到过其中任意一条报错,这篇文章也许能帮你省下几个小时。

它们看起来毫不相干,但在我这台机器上,最后都指向同一个隐藏原因——而三条报错里,没有任何一条提到它。

先说结论:

1
2
3
4
plutil -p ~/Library/LaunchAgents/actions.runner.*.plist \
| grep SessionCreate

# "SessionCreate" => true

SessionCreate 来自 GitHub Actions Runner 的默认 macOS plist 模板。通过官方 svc.sh install 安装服务时,它会被写入 Runner 的 LaunchAgent 配置。

注意:这三条报错并不总是等价,也不一定都由 SessionCreate 引起。本文记录的是它们在同一台机器、同一组实验中汇聚到同一个根因的过程。


症状:只有纯 SwiftPM 测试能通过

这是一个 macOS 菜单栏 App。CI 需要运行三组测试:

  1. 纯 SwiftPM 核心库单元测试:不打开 Xcode 工程,也不涉及签名
  2. App Target 单元测试:包含 TEST_HOST,需要构建并签名整个 App
  3. XCUITest

第一组一直是绿色。

第二组却卡在 CodeSign:

1
2
3
4
/…/IPPeek.app/Contents/Frameworks/XCUIAutomation.framework:
replacing existing signature
errSecInternalComponent
Command CodeSign failed with a nonzero exit code

签名身份明明存在:

1
Apple Development: ...

更奇怪的是,CodeSign 并非全部失败,而是每次随机坏掉一部分。

一轮构建有 9 次 CodeSign:

  • 有时失败 5 个
  • 有时失败 6 个
  • 有时只失败 3 个

每次失败的 Framework 还不一样。


第一条路:钥匙串锁了

errSecInternalComponent 经常与 Keychain 有关。

于是,我先在 CI 中打印钥匙串状态:

1
2
- run: security show-keychain-info \
~/Library/Keychains/login.keychain-db

Runner 中的输出是:

1
User interaction is not allowed

但在机器的 Terminal 中执行同一条命令,看到的却是:

1
no-timeout

我先按最常见的方式处理:

1
2
3
4
5
security set-keychain-settings \
~/Library/Keychains/login.keychain-db

security unlock-keychain \
~/Library/Keychains/login.keychain-db

重跑,依然失败。


第二条路:私钥 ACL

另一个经典原因是 Keychain 的 partition list。

security 的 man page 明确提到:如果要让 /usr/bin/codesign 使用私钥,partition list 中必须包含 apple:

于是执行:

1
2
3
4
5
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s \
-k "$PASSWORD" \
~/Library/Keychains/login.keychain-db

第一次执行后,失败反而更多了。

后来才发现,我中途重新同步过一次证书。

set-key-partition-list 修改的是执行当时已经存在的私钥 ACL。后来同步进来的私钥,自然没有获得相同配置。

重新执行一次后,失败数量从:

1
2
3
5/9

3/9

ACL 的确有影响,但它仍然不是根因。


第三条路:并发竞争

“每次失败的对象都不同”,实在太像竞争条件了。

于是,我写了一个最小探针:使用同一把私钥,分别串行和并行签名 9 个文件。

结果是:

1
2
Sequential failures: 9/9
Parallel failures: 9/9

串行同样全部失败。

这个实验至少证明:当前故障并不依赖并发,CodeSign 并发竞争不是主因。

这里有一个很重要的经验:

部分失败,不一定代表竞争。

看到随机失败时,我们很容易下意识怀疑锁、线程或并发。但更节省时间的办法,是先写一个最小实验。

十分钟的串行/并行对照,可能省掉后面几个小时。


真正的原因:Runner 不在登录会话里

重新整理所有现象后,有一个矛盾始终无法解释:

同一个用户、同一个钥匙串、同一时刻,看到的状态却不同。

执行上下文security show-keychain-info
Terminalno-timeout
Runner 作业内User interaction is not allowed

直到我检查 GitHub Actions Runner 的 LaunchAgent:

1
2
<key>SessionCreate</key>
<true/>

问题终于串起来了。

SessionCreate 会让 launchd 为该任务创建新的 security audit session。Apple 对 Security Session API 的说明也提到:创建新 session 会丢弃调用进程此前建立的安全信息,包括与 Keychain 相关的信息。

换句话说:

Runner 与登录用户打开的 Terminal,并不共享同一个 security session。

因此:

  • 在 Terminal 中解锁钥匙串
  • 在 Terminal 中确认授权弹窗
  • 在 Terminal 中看到某个 Keychain 状态

都不代表 Runner 所在的 session 会得到相同结果。

回头再看三个报错:

报错在本案例中的表现
errSecInternalComponentCodeSign 无法完成签名
Timed out while enabling automation modeUI 自动化授权无法在 Runner 上下文中完成,最终超时
The test runner hung before establishing connectionXCTest 无法继续建立测试连接

它们最终都落在同一条因果链上:

1
2
3
4
5
6
7
SessionCreate

Runner 进入独立的 security session

Terminal 中的 Keychain 解锁与授权无法直接作用于 Runner

CodeSign/Automation/XCTest 分别以不同方式失败

真正让我定位问题的,并不是报错本身,而是 Runner 与登录会话处在不同的 security session。

这也带来了第二个教训:

当“我明明改了,却没有生效”时,先确认修改发生在哪个上下文。

我花了三轮修钥匙串,最后才发现:我一直修的是 Terminal 所在的 session,Runner 根本不在里面。


为什么没有直接删除 SessionCreate

删除 SessionCreate 后,这三个问题的确一起消失了。

但新的问题也随之出现。

这台 Runner 同时服务多个仓库。如果让 Runner 直接进入登录用户的安全上下文,工作流就可能接触该上下文中已经解锁、且 ACL 允许当前工具访问的开发私钥,以及其他符合访问条件的 Keychain 条目。

对于一台服务多个仓库的共享 Runner,这不是我愿意接受的安全边界。

所以,我最终没有采用这个方案。

SessionCreate 不是一个单纯的“兼容性开关”。关闭它之前,应该先把它当作安全边界的变化来评估。


最后的取舍

最终,我把测试拆成三段:

  • SwiftPM 单元测试
  • App 单元测试
  • UI 测试

CI 始终运行第一段;后两段保留为本地门禁(Gate)。

这并不是因为后两段在技术上一定无法放进 CI,而是权衡之后,我不愿意为了几十条测试,让共享 Runner 与登录用户的 Keychain 安全上下文靠得太近。

工程里,很多时候没有绝对正确的答案。

只有清楚知道:自己得到了什么,又放弃了什么。


排查顺序

如果你也在配置 macOS Self-hosted Runner,可以按下面的顺序排查。

1. 检查 Runner 是否启用了 SessionCreate

1
plutil -p ~/Library/LaunchAgents/actions.runner.*.plist

2. 确认修改发生在哪个 security session

很多在 Terminal 中执行的解锁和授权,并不会自动作用到 Runner。

至少分别记录两个上下文中的:

1
2
security show-keychain-info \
~/Library/Keychains/login.keychain-db

3. 不要急着归因于竞争

如果失败对象看起来随机,先做最小实验,对比串行与并行结果。

随机只是现象,不是并发的证据。

4. 关闭 SessionCreate 前,先评估安全边界

它可能解决问题,但也会改变 Runner 所在的安全上下文。尤其是 Runner 服务多个仓库时,不要把它当作无成本修复。


总结

这一轮真正帮助我解决问题的,不是某条神奇命令,而是两个探针:

  • 在 Runner 内打印钥匙串状态
  • 用串行/并行实验验证竞争假设

很多时候,我们最大的敌人不是系统,而是自己的推断。

在一个无法直接观察的上下文里:

先测量,再假设。


参考资料

本文初稿由 Claude Code 生成,经 ChatGPT 修改润色。

排查 IPv6 网络时,很多人都会直接执行:

1
ping domain.com

但仅凭这一条命令,并不能完整验证网站是否真正支持双栈。

下面介绍一种适用于 macOS 的简单方法。

macOS 使用两套 Ping 工具

macOS 采用 BSD 工具链,分别提供:

命令作用
pingIPv4
ping6IPv6

与 Linux 不同,macOS 没有 ping -4ping -6ping4 等命令。

第一步:检查 DNS

先确认网站是否同时提供 A 和 AAAA 记录:

1
2
dig +short A bing.com
dig +short AAAA bing.com

如果两条命令都返回 IP 地址,则说明该网站具备双栈 DNS 解析能力。

第二步:验证连通性

分别测试 IPv4 和 IPv6:

1
2
ping bing.com
ping6 bing.com

如果两者都能够收到回复,则说明:

  • 网站同时支持 IPv4 和 IPv6;
  • 当前网络同时具备 IPv4、IPv6 连通性。

推荐测试目标

下面几个网站都适合作为双栈测试目标:

网站推荐域名
Microsoft Bingbing.com
淘宝taobao.com
QQwww.qq.com

注意: qq.com 根域名目前主要提供 IPv4,建议使用 www.qq.com 进行双栈测试。

一键检测脚本

下面这个脚本会自动检测:

  • A(IPv4)记录
  • AAAA(IPv6)记录
  • IPv4 连通性(ping
  • IPv6 连通性(ping6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env bash

set -euo pipefail

DOMAINS=(
"bing.com"
"taobao.com"
"www.qq.com"
)

resolve_ipv4() {
dig +short A "$1" |
awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print; exit }'
}

resolve_ipv6() {
dig +short AAAA "$1" |
awk '/:/ { print; exit }'
}

printf '| Domain | IPv4 (A) | IPv6 (AAAA) | ping | ping6 |\n'
printf '|---|---|---|:---:|:---:|\n'

for domain in "${DOMAINS[@]}"; do
ipv4="$(resolve_ipv4 "$domain")"
ipv6="$(resolve_ipv6 "$domain")"

if [[ -n "$ipv4" ]]; then
ping -c 1 "$ipv4" >/dev/null 2>&1 && p4="✅" || p4="❌"
else
ipv4="—"
p4="—"
fi

if [[ -n "$ipv6" ]]; then
ping6 -c 1 "$ipv6" >/dev/null 2>&1 && p6="✅" || p6="❌"
else
ipv6="—"
p6="—"
fi

printf '| `%s` | `%s` | `%s` | %s | %s |\n' \
"$domain" "$ipv4" "$ipv6" "$p4" "$p6"
done

实测结果

当前网络环境下,脚本输出如下:

DomainIPv4 (A)IPv6 (AAAA)pingping6
bing.com150.171.28.102620:1ec:33:1::10
taobao.com59.82.43.2382408:4001:f10::5e
www.qq.com111.30.185.1952409:8702:4860:1002::33

由于 DNS 会受到地区、运营商和 CDN 调度影响,不同网络环境解析到的 IP 地址可能不同。但只要同时存在 A、AAAA 记录,并且 pingping6 都能够正常连通,就说明当前环境下双栈工作正常。

总结

验证网站是否真正支持 IPv4 / IPv6 双栈,只需要四步:

1
2
3
4
5
dig +short A domain
dig +short AAAA domain

ping domain
ping6 domain

相比只执行一条 ping 命令,这种方式能够同时验证 DNS 解析和 IPv4、IPv6 的实际连通性,更适合作为日常网络排查和双栈测试的方法。

网络是否支持双栈,不是看一条 ping 命令,而是同时验证 DNS、IPv4 和 IPv6 连通性。这样得到的结论才更准确,也更有参考价值。

这篇文章是通过 ChatGPT 协助生成的。

OpenAI 发布 ChatGPT Work 后,很多人的第一反应是:

这不就是 Codex 吗?

我更新体验后的第一感觉也是如此。

但这次更新真正重要的,并不是 Work 本身,而是 ChatGPT 产品定位的变化

一句话总结:

ChatGPT 不再是一个聊天产品,而是一个 Agent 工作平台。

ChatGPT Work 首页


Chat 退居二线了

过去几年的 ChatGPT,首页最重要的位置永远留给输入框。

它告诉用户:

来聊天。

但新版 ChatGPT 的首页重点已经变成:

  • Work
  • Tasks
  • Codex

聊天输入框反而成了一个更小的入口。

这不是简单的 UI 调整,而是产品心智的变化。

产品首页永远留给最重要的能力。

现在,这个位置已经不属于 Chat。


Work 是 Codex 的泛化

很多开发者觉得 Work 像 Codex,我认为这个感觉没错。

两者的交互方式非常接近:

  • Goal 驱动
  • Agent 自主规划
  • 长时间执行
  • 后台任务
  • 多任务管理

真正变化的是能力边界。

以前 Codex 主要面向软件开发。

现在 Work 把同样的 Agent 模式扩展到了更多工作场景:

  • Coding
  • Documents
  • Browser
  • Gmail
  • Slack
  • Calendar

换句话说:

Codex 没有消失,而是成为了 Work 的一部分。

软件开发,只是 Agent 能完成的工作之一。


ChatGPT 正在变成工作平台

以前的 ChatGPT 更像是:

用户提问,AI 回答,然后结束。

现在则更像是:

用户给一个 Goal,AI 自己规划、调用工具、交付成果。

聊天已经不是产品核心。

真正的核心变成了:

完成任务(Task)

所以现在的 ChatGPT,更像一个工作平台,而不是聊天机器人。


为什么它越来越像 Claude?

ChatGPT Work 让很多人想到 Claude,这并不是巧合。

两家公司其实都在走向同一个方向:

1
2
3
4
5
Anthropic:
Claude → Claude Code → Claude Desktop → Cowork

OpenAI:
ChatGPT → Codex CLI → Codex Desktop → ChatGPT Work

区别只是起点不同。

Anthropic 是从开发者工具往外扩。

OpenAI 是从 ChatGPT 往工作平台扩。

最终,两者都收敛到了同一种产品形态:

Agent,而不是 Chat。


Cursor 面对的是新赛道

曾经,Cursor 几乎代表着 AI Coding。

但最近一年,大家讨论它越来越少。

我认为不是 Cursor 变弱了,而是赛道变了。

以前竞争的是:

谁补全最快?谁写代码最好?

现在竞争的是:

谁能独立完成整个工作?

Cursor 依然是一款优秀的 AI IDE。

但 OpenAI 和 Anthropic 已经把竞争提升到了 Agent 平台层面。

核心差别不再是“帮你写代码”,而是“帮你完成工作”。


我的看法

这次更新最值得关注的,不是新增了一个 Work,而是首页发生了变化。

首页决定产品心智。

过去打开 ChatGPT,是准备开始一段对话。

现在打开 ChatGPT,更像是在查看有哪些工作正在由 AI 帮你完成。

这是一次很小的界面调整。

也是一次很大的产品范式转变。

Chat,终于不再是 ChatGPT 的主角了。

这篇文章是通过 ChatGPT 协助生成的。

TL;DR:苹果验证的不是验证码,而是「你是否拥有一台受信任设备」。验证码只是授权流程中的确认码,并非第二因素本身。对于只有一台设备的用户,当前设备就是唯一的受信任设备。

很多人第一次使用 Apple ID 双重认证时,都会有一个疑问:

验证码都发到当前设备了,这还有什么安全意义?

我以前也一直没想明白。

后来查阅了 Apple 和 FIDO Alliance 关于 Passkey 的设计资料之后,我发现:

这个问题本身没有错,只是我们一直用传统 2FA 的思维,在理解 Apple 完全不同的一套认证体系。


结论

苹果验证的不是:

「你能不能收到验证码。」

而是:

「你是不是拥有一台受信任的 Apple 设备(Trusted Device)。」

验证码只是一次登录授权流程中的确认码。

因此,对于只有一台 Apple 设备的用户来说:

当前设备,就是唯一的受信任设备。

所以验证码出现在当前设备,并不是漏洞,而是苹果认证模型的设计结果。


身份认证,其实一直在解决上一代的问题

如果把整个身份认证的发展放在一起看,就很容易理解苹果为什么这么设计。

方案解决的问题新的问题
密码身份认证密码泄露、重复使用
短信验证码增加第二因素SIM 换卡、短信劫持、运营商成本
TOTP(Authenticator)不依赖短信需要备份、迁移、恢复
Apple Trusted Device不需要短信,不需要 Authenticator,可单设备完成当前设备授权容易引起误解
Passkey去掉密码、去掉验证码、抗钓鱼依赖生态支持(已逐渐普及)

其实每一代方案,都不是推翻上一代,而是在解决上一代留下的问题。


Apple 为什么没有采用传统 2FA?

传统双因素认证通常都是:

1
2
3
密码
+
另一台设备上的验证码

因为:第二因素最好来自另一台独立设备。这样即使电脑被攻击,攻击者也拿不到验证码。

这是很多银行、企业系统今天依然采用的方案。

但是 Apple 面对的是几十亿普通用户。它必须考虑:

  • 第一台 iPhone
  • 只有一台 Mac
  • 出门只带一台 iPad
  • 老年用户
  • 账号恢复

如果坚持验证码必须来自另一台设备,很多用户甚至无法登录自己的 Apple ID。

因此苹果做出了一个不同的选择:验证受信任设备,而不是另一台设备。

也就是说,只要这台设备已经建立了信任关系,它就可以完成整个授权流程。

这也是为什么验证码有时会显示在当前设备。


Apple 真正追求的是易用性

很多人觉得苹果这种设计是在牺牲安全。

我反而觉得:苹果是在追求普通用户也愿意使用高安全认证。

相比传统方案,Apple 的认证几乎没有额外负担:

  • 不需要短信
  • 不需要安装 Authenticator
  • 不需要保存 TOTP 种子
  • 不需要手动迁移验证码

整个过程,几乎都是系统自动完成。

对于普通用户来说,这比传统 2FA 简单太多了。


Passkey 才是真正的下一步

随着 Passkey 出现,苹果终于把最后一块拼图补齐了。

传统 Apple ID:

1
2
3
4
5
密码
+
受信任设备
+
验证码

Passkey:

1
2
3
4
5
受信任设备
+
Face ID / Touch ID
+
私钥签名

Face ID(或 Touch ID)验证的是:此时此刻操作设备的人,就是设备主人。

Secure Enclave 随后使用私钥完成签名。

因此:密码没有了。验证码也没有了。

整个登录过程几乎变成:Face ID。完成。


iCloud 同步,才是 Passkey 最大的优势

我认为,真正让 Passkey 成熟的,并不是 Face ID,而是 iCloud Keychain

过去,TOTP 最大的问题其实不是安全,而是:

  • 换手机要迁移
  • 忘记备份
  • 手机坏了账号恢复困难
  • 需要保存恢复码

管理成本非常高。

而 Passkey 在 Apple 生态里变成了:

1
2
3
4
5
6
Secure Enclave 生成私钥

iCloud Keychain 端到端加密同步

Mac / iPhone / iPad / Vision Pro
自动拥有同一套 Passkey

这里同步的不是密码,而是经过端到端加密保护的 Passkey。

于是苹果第一次真正做到:既安全,又几乎不用用户管理。

这是过去几十年身份认证一直很难兼得的事情。


安全设计,从来都是权衡

有人会说:如果电脑完全被攻破,独立设备验证码是不是更安全?

答案是:是。

物理隔离,永远是一种安全。

所以政府、企业管理员、高安全行业,今天依然大量使用:

  • 硬件安全密钥
  • 独立认证设备

但是苹果面对的是几十亿普通用户。它需要的是整体最优,而不是某一种攻击模型最优。

因此苹果每一代身份认证,其实都在做同一件事情:

在安全性、易用性、恢复能力之间不断寻找新的平衡。


最后

现在再回到最初那个问题:为什么苹果会把验证码发到当前设备?

答案其实已经很简单了。

因为苹果验证的,从来不是验证码,而是受信任设备。

验证码只是那个时代,完成授权流程的一种方式。

而随着 Passkey、Secure Enclave、Face ID 和 iCloud Keychain 的成熟,苹果终于把整个认证流程进一步简化成:

拥有设备 + 证明现在就是你。

用户几乎感觉不到复杂的密码学、密钥管理和同步机制。

这恰恰是我认为苹果这些年身份认证设计最成功的地方。

真正优秀的安全设计,不是让用户做更多,而是让用户几乎感觉不到安全本身。

这篇文章是通过 ChatGPT 协助生成的。

TL;DR:macOS 自带 curl 没编译 HTTP/3,浏览器能连是它自己的网络栈。装 Homebrew 版:brew install curl,加到 PATH 即可。

最近在验证网站是否开启了 HTTP/3,发现一个容易踩的坑。

浏览器访问 https://http3.is/,开发者工具已经显示:

1
Protocol: h3

说明浏览器确实使用了 HTTP/3。

但终端执行:

1
curl -I --http3 https://http3.is/

却提示:

1
curl: option --http3: the installed libcurl version doesn't support this

原因很简单:macOS 自带的 curl 没有编译 HTTP/3 支持。

浏览器 vs curl

项目浏览器(Safari / Chrome)macOS 自带 curlHomebrew curl
网络栈浏览器自带系统 libcurl最新 libcurl
HTTP/2
HTTP/3
QUIC
是否推荐测试 HTTP/3

查看当前 curl 是否支持 HTTP/3:

1
curl -V

如果 Features 中没有 HTTP3,就无法使用 --http3

解决方案

安装 Homebrew 版本:

1
brew install curl

将 Homebrew 的 curl 放到 PATH 前面:

1
2
echo 'export PATH="/opt/homebrew/opt/curl/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

确认已经切换:

1
2
which curl
curl -V

输出中应包含:

1
Features: ... HTTP2 HTTP3 ...

验证

1
curl -I --http3 https://http3.is/

返回:

1
HTTP/3 200

说明已经成功通过 HTTP/3(QUIC)建立连接。


一句话总结:

  • 浏览器验证:看开发者工具 Protocol: h3
  • 终端验证:使用支持 HTTP3 Feature 的 Homebrew curl。
  • macOS 自带的 curl 即使版本很新,也不一定支持 HTTP/3

这篇文章是通过 ChatGPT 协助生成的。

如果 Xcodes 一直提示:

1
2
Helper is not installed
cannot enable

Xcodes Helper is not installed

而且无法重新安装 Privileged Helper,可以先卸载旧的 LaunchDaemon:

1
sudo launchctl bootout system /Library/LaunchDaemons/com.xcodesorg.xcodesapp.Helper.plist

然后重新打开 Xcodes,点击 Install Helper,即可重新安装。

我这里就是旧的 Helper 状态异常导致安装失败,执行 bootout 后恢复正常。希望能帮到遇到同样问题的人。

这个问题是通过 ChatGPT 协助定位并解决的。

在 Apple Silicon Mac 上运行 iOS App 时,遇到一个很奇怪的问题:同一个 App,当前 Apple ID 打不开,换另一个 Apple ID 却可以正常运行。

这类问题一开始很容易怀疑是 App 没有启用 Mac 兼容性,或者是 SDK、系统版本兼容问题。但如果同一个 App 只换 Apple ID 就能运行,基本可以把方向收敛到账户授权层面。

日志线索

重新登录 App Store、清理缓存都没有效果后,通过 log stream 抓实时日志,看到关键线索:

1
2
3
fairplayStatus: -42005
Could not find account, trying to make one from store metadata
Could not find account, using the active account instead

这里的重点是 fairplayStatus: -42005。它指向本地 FairPlay DRM 授权数据异常,可能是密钥、证书或账户授权状态损坏。

修复方法

可以清除本地 FairPlay 授权数据,然后重启,让系统重新生成:

1
2
3
4
5
6
7
8
9
killall -9 appstoreagent storeaccountd fairplayd
rm -rf ~/Library/Application\ Support/com.apple.fairplayd
rm -rf ~/Library/Caches/com.apple.fairplayd

# 以下两步可能因为 SIP 保护被拒,但不影响这次修复
sudo rm -rf /private/var/db/fpsd
sudo rm -f /private/var/db/scinfo/*

sudo shutdown -r now

重启后,系统会重新建立 FairPlay 授权数据。我的情况是执行后问题恢复正常。

总结

如果 iOS App 在 Mac 上无法运行,而且同一个 App 换 Apple ID 后可以打开,可以优先排查 FairPlay 授权状态。

fairplayStatus: -42005 是一个很关键的诊断信号。相比反复重装 App 或重新登录 App Store,直接看 appstoreagentstoreaccountdfairplayd 相关日志会更快定位问题。

这个问题是通过 QClaw 协助定位并解决的。

Introducing

Pingman, your new essential network utility for iPhone, iPad, and Mac. Built entirely with Swift and SwiftUI, Pingman offers a sleek and intuitive way to check the reachability of hosts on your network and across the internet. Whether you’re a network professional, a developer, or simply curious about your network connectivity, Pingman provides the tools you need right at your fingertips.

Features

Pingman allows you to quickly and easily send ICMP echo requests to target hosts, displaying real-time results including response times and packet loss. Its clean and modern interface, thanks to SwiftUI, provides a consistent experience across all your Apple devices. You can easily monitor network stability, diagnose connection issues, and verify the availability of servers and websites.

Free Features:

  1. Effortless Ping Tests: Conduct basic ping tests with default settings for quick network checks.
  2. Clear Ping Summaries: Receive a concise summary at the end of each ping test, highlighting key statistics.
  3. Organized Target Lists: Easily manage your frequently pinged targets with preset options, a history of previous targets, and a bookmarking feature for quick access.
  4. Smart Notifications: Get instant alerts when ping failures occur and when services recover.

Pro Features:

  1. Advanced Ping Control: Unlock additional parameters to fine-tune your ping tests for specific network analysis needs.
  2. Seamless Log Management: Share and save detailed ping logs for documentation, analysis, or troubleshooting purposes.
  3. Data Export & Import: Backup and restore your bookmarks, history, and logs across devices.

If you have any questions or suggestions, you can contact them through Email.

Download Pingman on the App Store

Terms of Use

Apple Media Services Terms and Conditions

Privacy policy

This App does not collect or upload any private information.

1 安装

1.1 使用命令行

1
2
3
4
5
$ git clone https://github.com/giginet/Scipio.git
$ cd Scipio
$ swift run -c release scipio --help
# Add reference .build/release/scipio to the PATH variable.
$ export PATH=/path/to/scipio:$PATH

1.2 作为Package使用

推荐使用这种方式,较少命令行中参数,在Swift代码中EntryPoint配置方便。

2 准备您应用程序的所有依赖项

2.1 创建一个新的Swift包来描述依赖关系

1
2
3
$ mkdir MyAppDependencies
$ cd MyAppDependencies
$ swift package init

2.2 编辑 Package.swift 以描述应用程序的依赖关系下一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MyAppDependencies",
platforms: [
// Specify platforms to build
.iOS(.v14),
],
products: [],
dependencies: [
// Add dependencies
.package(url: "https://github.com/onevcat/APNGKit.git", exact: "2.2.1"),
],
targets: [
.target(
name: "MyAppDependency",
dependencies: [
// List all dependencies to build
.product(name: "APNGKit", package: "APNGKit"),
]),
]
)

3 手动Rswift generate到项目中

如何找到R文件:R.generated.swift

任意找到一个_R,点击定义,FileShow in Finder

4a 命令行打包

1
2
3
4
5
6
7
8
$ scipio prepare path/to/MyAppDependencies
> 🔁 Resolving Dependencies...
> 🗑️ Cleaning MyAppDependencies...
> 📦 Building APNGKit for iOS
> 🚀 Combining into XCFramework...
> 📦 Building Delegate for iOS
> 🚀 Combining into XCFramework...
> ❇️ Succeeded.

4b 自定义打包配置

4b.1 创建可执行包

1
2
3
4
5
6
7
8
$ mkdir my-build-tool
$ cd my-build-tool
$ swift package init --type executable
Creating executable package: my-build-tool
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift

4b.2 编辑Package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "my-build-tool",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(
url: "https://github.com/giginet/Scipio.git",
revision: "0.15.0" // Use the latest version
),
],
targets: [
.executableTarget(
name: "my-build-tool",
dependencies: [
.product(name: "ScipioKit", package: "Scipio"),
],
path: "Sources"
),
]
)

4b.3 实现构建脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
import ScipioKit

@main
struct EntryPoint {
private static let myPackageDirectory = URL(fileURLWithPath: "/path/to/MyPackage")

static func main() async throws {
let runner = Runner(
mode: .prepareDependencies,
options: .init(
baseBuildOptions: .init(
buildConfiguration: .release,
isSimulatorSupported: true
)
)
)

try await runner.run(
packageDirectory: myPackageDirectory,
frameworkOutputDir: .default
)
}
}

4b.4 命令行打包

1
$ swift run -c release my-build-tool

引用

  1. https://github.com/giginet/Scipio.git
  2. https://github.com/mac-cain13/R.swift/blob/main/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift

示例代码🔗https://github.com/gewill/BlogCodes/tree/main/Localizable%20in%20SwiftPM

在处理SwiftPM中本地化时,尝试了几种方案。先说结论Rswift preferredLanguage方案最佳。

方案一:Local

在SwiftUI中使用local可行,但是在SwiftPM会被宿主应用中覆写。不过也是小问题,只要命名规范,按照模块页面功能前缀来的话,一般也不会出现key重复的问题。

这里也是用到了Rswift自动生成的key,避免复制粘贴字符串类型的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SwiftUI view			
Section {
Text("Change locale").font(.title)
Text("Will be overwrite by host app!").foregroundColor(.pink)
Button(action: {
viewModel.locale = Locale(identifier: Language.en.rawValue)
}, label: {
Text("Change locale english")
})
Button(action: {
viewModel.locale = Locale(identifier: Language.zh_Hans.rawValue)
}, label: {
Text("Change locale chinese simplified")
})
Text(LocalizedStringKey(R.string.localizable.hello_world.key.description))
} header: {
Text("Change locale")
}
.environment(\.locale, viewModel.locale)

方案二:Rswift preferredLanguage

目前是比较完善的方案。配合 AppLocale 可以全局切换语言。

利用Rswift可处理key和bundle的问题,还优化了SwiftUI.Text的使用体验,直接使用init即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AppLocale {
let preferredLanguage = CurrentValueSubject<Language, Never>(.en)
var preferredString: _R.string {
R.string(preferredLanguages: [preferredLanguage.value.rawValue])
}

static var shared = AppLocale()
private init() {}
}

// SwiftUI view
Section {
Text("Preferred Languages \(viewModel.preferredLanguage.displayTitle)")
Picker("Preferred Languages", selection: $viewModel.preferredLanguage) {
ForEach(Language.allCases) {
Text($0.displayTitle)
}
}
.pickerStyle(.segmented)
Text(AppLocale.shared.preferredString.localizable.hello_world)
} header: {
Text("Change R.string Preferred Languages")
}

最轻量级集成方式在ViewModel订阅AppLocale.shared.preferredLanguage,更新self.objectWillChange.send(),即可响应语言切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HomeViewModel: ObservableObject {
init() {
AppLocale.shared.preferredLanguage
.removeDuplicates()
.sink(receiveValue: { _ in
guard let self else { return }
self.objectWillChange.send()
})
.store(in: &cancelables)
}
}

struc HomeView: View {
@StateObject var viewModel = HomeViewModel()

var body: some View {
Text(AppLocale.shared.preferredString.localizable.hello_world)
}
}

方案三:liamnichols / xcstrings-tool

可用,但是仅支持 iOS16+。有个小坑SwiftPM集成时,官方教程的有错误,正确的git地址为:

1
2
3
4
// 1. Add the xcstrings-tool Package dependency
.package(url: "https://github.com/liamnichols/xcstrings-tool.git", from: "0.1.0")
// 2. Or use the repo is essentially a mirror of the main repository however the xcstrings-tool command line interface is a binary dependency that significantly simplifies your build graph and improves compile times.
.package(url: "https://github.com/liamnichols/xcstrings-tool-plugin.git", from: "0.1.0")

具体参考官方的示例:https://github.com/liamnichols/xcstrings-tool-demo