命令行参考
两个命令的全部参数、两张退出码表,以及如何阅读返回的内容。带有设计思路的叙述版在 CLI 与 CI 页面。
两个命令
chrono run 以另一个日期和时间启动应用。chrono calc 算出一个日期,并告诉你它为什么值得测。两者共用同一套步骤语法,所以为其中一个写的偏移,在另一个里含义完全相同。
chrono version 回答第三个小得多的问题:这是哪个构建、可执行文件是两个内核中的哪一个,以及它使用的协议版本。在 stdout 上只输出一行,方便脚本捕获。
chrono license 回答第四个问题:这个构建以什么许可证分发、免责声明,以及链接进来的第三方组件。它还会告诉你完整许可证文本和第三方声明在磁盘上的位置;如果你运行的这份副本旁边没有它们,也会如实说明。 --components 会列出每个随附组件及其版本,而不是只给出摘要。
chrono presets 按 --preset 读取的方式列出预设目录:文件夹中的每个预设,用计算器语法读取的词语写出,以及每个会被拒绝的文件和原因。--json 输出 chronomock.presets/1,--dir 指定另一个文件夹。
chrono run <target> [options] chrono calc [options] chrono version # chrono 0.4.0 (x64, protocol 1) chrono license # GPL-3.0-only,以及随附了什么 chrono presets # 按 --preset 读取方式列出的目录 chrono --help # 查看所有命令的用法
chrono run——选项
| 参数 | 作用 |
|---|---|
--at <moment> |
起始时刻,按会话时区书写。可以是绝对时间(2038-01-19T03:14:07),也可以是带前导符号的相对时间(+1d、-2h)。单位:s m h d w mo q y——m 是分钟,mo 是月。不带此参数时,会话时钟从真实的当前时间开始,因此单独使用 --mode xN 只是让目标程序跑得更快。 |
--zone +HH:MM |
会话时区:既是书写该时刻所用的时区,也是目标程序的本地时间调用所报告的时区。固定偏移量,没有夏令时。不带此参数时使用宿主机自己的时区。 |
--mode flow|frozen|xN |
flow 按真实速度运行,frozen 让时钟保持不动,xN 让它快 N 倍(x60 让一小时变成一分钟,x1440 让一天变成一分钟)。 |
--scale-duration |
同时加快经过时间轴:滴答计数、中断时间、休眠和定时器。对象等待只计数并保持真实时长。默认关闭,因为墙上时钟和秒表是两个不同的问题。设定在某个日期和时间的定时器或延迟,无论有没有这个选项都会跟随会话时钟。 |
--scale-qpc |
同时缩放高分辨率计数器,Python 3.13+ 的 monotonic、.NET 的 Stopwatch 和 Java 的 nanoTime 都是从这里读取经过的时间。默认关闭:用该计数器给渲染计时的应用可能会显得画面失真。 |
--ticks N |
在 N 次心跳(每次一个真实秒)之后结束会话并报告。不带此参数时,工具会一直保持连接,直到目标程序以及它启动的、运行在会话时钟上的所有程序都退出——立即结束的启动器会把会话留给它所启动的应用。 |
--timeout <s> |
超过这么多秒就放弃并以 6 退出,适用于不允许挂起的流水线。与此参数无关,内核如果 15 秒没有任何响应,也会被停止并同样以 6 退出——两种情况下运行都被中途截断,所以都不带判定。 |
--args "..." |
传给目标程序的参数,写成一个字符串,按 Windows 使用的同一规则拆分——因此含空格的参数可以完整保留。对于 Chromium 目标,--user-data-dir 和 --remote-debugging-port 会被拒绝而不是被覆盖:会话需要自己独立的配置文件,而悄悄地在你真实的浏览器配置文件上运行,是绝不能发生的一件事。 |
--cwd <dir> |
在该目录中启动目标程序。不带此参数时,目标程序继承我们自己的工作目录,这也是此参数出现之前每次运行的行为。空值是用法错误,而不是“没有目录”——未提供和为空是两回事,只有未提供才是 Windows 能接受的。目录不存在时,会话会在启动任何东西之前以 target.cwd_missing 终止,而不是等到后来表现为目标程序无法启动。 |
--preset <id> |
从 presets/<id>.json 中取得时刻和速度。与 --at、--mode 和 --scale-duration 互斥,因为此时预设是这两者唯一的来源。 |
--param id=value |
填写预设的参数。可重复使用,需要搭配 --preset。仅在 run 中,试用期开始日期默认取目标可执行文件自己的创建日期,因此试用类预设会从应用安装的那一天开始计算,无需你手动输入。 |
--set-after T:M |
在第 T 次心跳时,把速度改为 M。运行过程中倍率至少为 1——这里不提供会话中途冻结。 |
--jump-after T:moment |
在第 T 次心跳时,把时钟跳到该时刻,绝对或相对均可,按会话时区解释。 |
--force |
即使开场判定表明替换没有生效,也保留会话——默认会停止目标程序,因为什么也没证明的会话比没有会话更糟。判定不会改变,退出码也不会改变。它只决定目标程序是否继续运行。 |
--no-embedded |
让应用内部的网页(WebView2、Qt WebEngine)保持在真实时钟上,并且不打开调试端口。不加此参数时,会话会请这类引擎打开本地端口,并通过它把其页面放到会话时钟上。端口开启期间,这台计算机上的任何其他程序都可以用它访问这些页面。 |
--elevated-embedded |
用于以管理员身份运行的应用,需要从同样以管理员身份运行的 Chrono Mock 启动。对于这类应用,WebView2 会忽略会话用来访问其页面的设置,所以会话在持续期间向计算机注册表(HKLM,以应用的 .exe 命名)写入一个值,并在结束时删除。应用运行期间,这台计算机上的任何其他程序都可以通过该端口访问其页面,而该应用拥有管理员权限。不能与 --no-embedded 同用。 |
--keep-background-timers |
启动 Web 引擎(Chromium 或 Electron 应用的浏览器,或应用内部的引擎)时关闭其对隐藏窗口中定时器的减速,这样最小化的窗口仍保持会话速度。此时应用在隐藏状态下也会全速运行。 通过 --elevated-embedded 以管理员身份启动的应用不会应用此设置。 |
--dry-run |
只打印会话将会是什么样子,不启动任何东西:解析出的时刻和时区及其来源、预设为每个参数填入了什么、拆分后的参数,以及目标程序将采用两种机制中的哪一种。不会启动任何进程,也不会写任何文件,包括 --report 指定的那个。计划可行时以 0 退出,目标程序不存在时以 2 退出,这与真实运行遇到同样情况时给出的退出码一致。计划不带判定,并且会如此声明——只有真实的会话才有判定。 加上 --json 时,计划以数据形式给出,其中包括 session.elevated_embedded 和 session.elevated_embedded_value。后者是在该选项适用于 .exe 目标时注册表值将以其命名的文件名,其他情况为 null。这里有名称并不保证一定会写入:只有 Chrono Mock 本身以管理员身份运行时,会话才会写入该值。 |
--report <path> |
把给人读的报告写入文件。判定不是生效时,该文件开头会有一条证据不可靠的横幅,因为隐藏自身疑点的证据比没有证据更糟。判定为生效的会话,如果应用程序没有读取会话所替换的任何时钟,或者它是 Chrono Mock 够不到其墙上时钟的 Go 程序,也会带上这条横幅。文件还会写明这次运行本身:版本、开始时间、目标程序、其文件夹和各个选项。 |
--report-args |
与 --report 一起使用时,也会把目标程序的参数原样写入该文件。默认关闭,因为参数里可能带有密码,此时文件只写明参数的个数。 |
--json |
以机器可读的记录输出会话,而不是报告——与窗口自己使用的是同一个数据流。控制台应用自己的输出会写到标准错误,因此标准输出里只有这些记录,没有别的东西。会话结束时仍在运行的应用会继续往那里写,所以任何一直读到标准错误末尾的程序都要等它,而这次运行会如实说明。 |
chrono run——退出码
退出码是会话的判定,而不是应用返回的值。应用自己的退出码会单独报告,写在报告和会话数据中——CLI 与 CI 页面解释了为什么要这样设计。
| 退出码 | 含义 |
|---|---|
| 0 | 生效——所有读取过时间的进程看到的都是会话时钟 |
| 10 | 部分生效——部分通道已覆盖,部分未覆盖 |
| 11 | 未生效——替换没有到达关键通道 |
| 4 | 无法确定——无法确认覆盖情况。是一句诚实的“无法判断”,绝不假装通过 |
| 12 | 目标程序在注入后立刻消失,并且没有留下任何运行在会话时钟上的东西,这表明它可能是单实例应用,把工作交给了已在运行的副本,或者交给了会话无法进入的程序,通常是位数不同的另一个程序。启动应用后就结束的目标程序属于启动器,会话会继续跟随它启动的那个应用 |
| 1 | 用法错误——参数错误、表达式错误,或是此命令不接受的预设 |
| 2 | 无法启动目标程序,或无法向其注入 |
| 3 | 内部错误——引擎无法启动、遇到意外状态,或在关闭会话之前就停止了(无论目标程序自己的退出码是什么)。这样的运行不带判定 |
| 5 | 此版本未内置该操作——例如某个预设的时刻需要日历,而 run 目前还无法加载日历。刻意与用法错误区分开:你要求的是目前还不存在的东西,而不是错误的东西 |
| 6 | 运行被中途截断——--timeout 到期,或者引擎 15 秒没有响应而被停止。这不是判定:会话被中断了,因此对目标程序无论哪种情况都说明不了什么 |
| 7 | 已有另一个会话在运行,一次只能运行一个会话。等它结束后重试即可 |
| 8 | 会话已得出判定,但证据没有写入 --report 指定的文件:磁盘已满,或另一个程序打开着旧文件,旧文件因此保持不变。判定仍在命令输出的报告中 |
如何阅读报告
不带 --json 时,会话以一份纯文本报告结束。每一行要么是会话观察到的事实,要么就不出现——没有任何内容是用看似合理的默认值填上去的。
Chrono Mock - session report
target: .\build\test.exe
verdict: WORKS - time substitution took effect (processes: 1)
every process that read time saw the session clock
session: fake clock reached 2038-01-19T03:14:08
real elapsed 1.3s, fake elapsed 1.3s
exited: the target closed itself with code 3
covered channels (substituted, with call counts, 10 calls in all):
- pid 21240: GetSystemTimeAsFileTime (5 calls)
- pid 21240: GetLocalTime (5 calls)
observed channels (hooked but left real):
- pid 21240: NtCreateUserProcess (0 calls)
warnings:
- the target opened a network connection ...
- verdict——针对父进程及所有子进程得出的整体判定,附带进程数。退出码承载的就是这个数。
- exited——应用自己的退出码,而且只在它自行关闭时才有。由
--ticks限定的运行会让目标程序继续运行,Chromium 会话是由工具自己关闭的,从未启动的会话也没有这样的退出码——这时这一行会直接不出现,而不是显示一个编造的零。崩溃码会以很大的负数形式出现,所以也会给出它的十六进制形式。 - covered——替换到达的通道,以及每个通道实际被调用的次数,按进程分别统计。次数绝不会跨进程相加:一个进程的审计不能借用另一个进程的数字。计数在 1048576 处停止,之后显示为至少这么多次。
- observed——已挂钩但刻意保持真实的通道,例如对象等待和多媒体定时器。它们自成一类,因此绝不会被当作已替换。
- uncovered——应用请求过但会话没有覆盖的通道。正是它让判定变成部分生效。
- not fully cleaned up——清理阶段无法移除的任何东西。正常结束时不会出现,这也是常态。
chrono calc——选项
不支持自然语言:每个参数就是一个步骤,参数的顺序就是步骤的顺序。每一步之后的结果都会打印出来,所以哪一步出错一目了然,而不是被埋起来。
| 参数 | 作用 |
|---|---|
--base today|now|<moment> |
从哪里开始,默认是 today。按会话时区解析。像 2025-02-31 这样不可能存在的日期会被拒绝,绝不会悄悄规整到下个月。 |
--base-utc <moment> |
与 --base 类似,但起点是 UTC 中的一个时刻(末尾可带 Z),而不是会话时区中的墙钟读数。 |
--shift ±N<unit> |
可重复使用。必须带符号。单位:s m h d w mo q y bd,或者它们的全称。m 是分钟,mo 是月。月、季度和年按日历日期折算,并做截断处理,因此 1 月 31 日之后的一个月是 2 月月底,而不是固定数目的滴答。bd(工作日)需要 --calendar。 |
--set-time HH:MM:SS | 在当前结果上设定一天中的时间。 |
--snap <target> |
跳到某个周期的边界:som eom soq eoq soy eoy(月、季度、年的开始或结束)。开始是该日的 00:00:00,结束是最后一天的 23:59:59。 |
--nearest nbd|pbd |
向前或向后最近的工作日,如果当前日期本身就是工作日则取当天。需要 --calendar。 |
--to-zone +HH:MM |
一个步骤:用另一个固定偏移量重新表示同一个时刻。时刻保持不变,所以纪元值不动,变的只有民用时间字段。不要与 --zone 混淆。 |
--zone +HH:MM |
会话时区,用于解析 today 和 now 并渲染结果。不带此参数时使用宿主机的时区。 |
--calendar us-banking|us-federal|pl |
加载一份节假日日历。它会在元数据中加入工作日和节假日,并启用 bd 和 --nearest。两份美国日历的区别在于遇到周末的节假日如何顺延,这恰恰是测试要找出的那种差一天的错误。 每个日历只对一段年份作答:pl 自 2002 年起,us-federal 自 1960 年起,us-banking 自 2008 年起。工作日步骤如果到达这个范围之前的日子,会以退出码 1 被拒绝,范围之前的日期则显示为未作判断。如果日历的节假日上次对照法律检查已超过一年,它所判断的较晚日期会被标记为 calendar_outdated,因为可能缺少较新的节日。 |
--preset <id> --param id=value |
来自 presets/<id>.json 的命名时刻,其参数已填入。与步骤参数以及 --analyze 互斥,并且可与 --calendar、--zone 和 --format 组合使用。 |
--analyze <date> |
读回一个粘贴进来的日期,并说明它是什么。有歧义的 04/08 会以美国和波兰两种读法同时显示,而不是靠猜——这种歧义确实是缺陷报告出错的一个来源。 |
--format <mask> |
同时用你自己的格式掩码打印结果,使其与被测应用的格式一致:yyyy-MM-dd HH:mm:ss。标记区分大小写,沿用 .NET 和 Java 的含义——M 是月,m 是分钟。 |
--json |
为上述任何一种用法提供机器输出(chronomock.calc/1),让流水线可以算出一个日期,并直接把它传回 chrono run。 |
# 本季度的最后一个工作日,使用美国银行业日历 chrono calc --base today --snap eoq --nearest pbd --calendar us-banking
chrono calc——退出码
单独的一张更小的表:计算器没有覆盖判定可以报告。
| 退出码 | 含义 |
|---|---|
| 0 | 已算出 |
| 1 | 用法错误——参数错误、不可能存在的日期、超出范围,或者日历文件里一个工作日都没有 |
| 5 | 此版本未内置该步骤或单位,或者该步骤需要你没有传入的日历。特意与用法错误分开,让脚本能区分“尚未支持”和“写错了” |