你给某个每天定时跑的任务写了个 .bat,里面先 echo 打个开始标记,再调用真正的业务程序,最后留个结尾标记。安静地跑了几周,直到某天核对数据,才发现最新数据停在将近三周前——而计划任务每次都老老实实报告「上次结果:0(成功)」。
现象
翻日志能看到两个反常的地方。
第一,日志不完整。原本应该有三段内容——echo start、业务程序的输出、结尾标记——结果只剩下孤零零的结尾标记,中间的开始标记和业务调用被静默吞掉了,不是报错,就是没有。
第二,任务状态骗人。任务计划程序里「上次运行结果」显示 0x0,也就是成功。它根本不报错。
两件事放一起,就成了最难查的那种故障:系统说一切正常,数据却悄悄停了。
根因
这个 .bat 有两个问题叠在一起,而且它们会互相放大。
一是换行符。 文件是 LF 换行(Unix 风格),而 Windows 的 CMD 解析批处理时需要 CRLF。CMD 的解析器是按行读的,它认的「行结束」是 \r\n。给了它 LF 结尾的文件,行与行的边界在它看来是错乱的,命令会被拼接到一起或截断。
二是编码。 文件里有 UTF-8 编码的中文注释。CMD 按系统代码页(简体中文下是 GBK)读取 .bat。UTF-8 的中文字节被当成 GBK 之后,不仅显示为乱码,更关键的是字节边界错位,可能把某一行后面跟着的换行符或命令关键字「吃掉」或「拆开」。于是后面几行干脆没被当成独立命令——这就是命令被静默吞掉的直接原因。
两者叠加的效果是:CMD 读到某一行时解析失败或者把多行合并了,它不会停下来报错,而是继续往下执行(或跳过),最后跑到文件尾,进程正常退出。退出码是 0,因为「跑完文件」这件事本身没出错。
解决
定两条硬规矩,所有给 Windows 用的 .bat 都遵守:
- 一律 CRLF 换行;
- 一律纯 ASCII——中文注释也换成英文。
可以写一个校验脚本,在提交或部署前断言「没有裸 LF,且全是 ASCII」:
data = open('task.bat', 'rb').read()
assert b'\n' not in data.replace(b'\r\n', b''), '存在裸 LF,需要转成 CRLF'
assert all(b < 128 for b in data), '存在非 ASCII 字节'
第一行先去掉所有合法的 \r\n,如果还剩 \n,说明有裸 LF。第二行检查每个字节都小于 128(ASCII 范围)。
改完之后,必须用真实调度触发一次,而不是手动双击:
schtasks /run /tn "任务名"
然后核对产出的数据日期,而不是只看任务返回值——返回值正是这一路把你骗过去的东西。
延伸与预防
这条坑的教训分两层。
技术层面:「写好的脚本能跑」和「文件字节是对的」是两回事。跨平台、跨工具搬运文本时,换行符和编码是必须显式确认的两个属性,不能靠默认。
运维层面更重要:任务返回成功不等于业务成功。 退出码只说明进程没崩,不说明它做了该做的事。判断一个定时任务是否健康,要么看它的产物(数据日期、文件大小、行数),要么让脚本自己写日志并监控日志里的关键节点。只看任务调度器的状态,就是给自己埋一个「三周后才发现」的雷。
同样的逻辑适用于所有无人值守的自动化:你的验证点必须落在「业务结果」上,而不是「程序有没有退出」。