首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >#WorkBuddy# 定时任务「没跑」和「跑一半卡住」,多半不是调度器的问题:9 个中断点体检清单(附 macOS 零依赖脚本)

#WorkBuddy# 定时任务「没跑」和「跑一半卡住」,多半不是调度器的问题:9 个中断点体检清单(附 macOS 零依赖脚本)

原创
作者头像
用户12406184
发布于 2026-11-01 00:18:53
发布于 2026-11-01 00:18:53
2130
举报

#WorkBuddy# 定时任务「没跑」和「跑一半卡住」,多半不是调度器的问题:9 个中断点体检清单(附 macOS 零依赖脚本)

一、你会遇到的场景

我踩过最难受的一次不是任务报错,而是它根本没报错。

早上九点该出的东西没出,打开一看:那条任务昨天的执行记录是空的,没有失败、没有重试、没有告警。翻日志什么都没有。你不知道它是没被触发,还是触发了、跑到一半卡在某个你看不见的地方。

这类问题有个共同点:在任务里加 try/except 没用,加重试也没用——因为它卡的不是代码逻辑,是代码和你这台机器之间的那层环境。

顺带说个热点:10 月 28 日 WorkBuddy 上线了「远程控制」,手机上能看电脑桌面的实时画面并直接接管【实证,二手来源:钛媒体/腾讯网 10-28 报道】;据 Pandaily 同日报道,官方称远程控制走流量、不消耗积分。这功能解决的是「卡住之后我能接手」。但更好的做法是让它根本不卡——下面这份清单就是干这个的。

二、结论先行

定时任务出问题,先别查调度器,先体检「中断点」。

我把它分成两类,排查顺序完全不同:

症状

大概率原因

先看哪几项

根本没跑(无执行记录)

调度器没登记 / 机器睡了 / 时区错位

睡眠、调度器登记、时区

跑了但产物残缺或卡死

PATH 变了 / sudo 要密码 / 有交互提示 / 盘满了

PATH、sudo 免密、TTY、磁盘

一句话边界:这套体检只管「环境会不会把任务卡住」,不管业务逻辑对不对。 业务正确性得另一套校验(比如产物行数、金额勾稽),别指望它。

三、完整操作步骤(macOS)

步骤 1:先分清是「没跑」还是「跑一半」

看两件事就够:

  • 有没有执行记录 → 没有 = 调度层问题(步骤 5、6)
  • 有记录、退出码 0、但产物不对 → 环境层问题(步骤 2、3、4)

步骤 2:体检 PATH——最隐蔽的一刀

macOS 上 cron 的默认 PATH 是 /usr/bin:/bin。你在终端里装的东西,多半在 /opt/homebrew/bin 或自己的 ~/.xxx/bin 下,cron 全看不见。

代码语言:bash
复制
# 终端里(你在的环境)
command -v ffmpeg

# 模拟 cron 的环境
PATH=/usr/bin:/bin command -v ffmpeg

两条输出不一样,就是中招了。

步骤 3:体检交互提示——TTY 和 sudo

无人值守环境下,stdin 不是终端。任何 input()、密码提示、确认框都会让任务安静地挂在那里直到超时。

代码语言:bash
复制
sudo -n true; echo "退出码=$?"
# 返回 1 = 需要输密码 = 无人值守必卡

-n 是非交互模式,不会真的弹密码框,可以放心跑。

步骤 4:体检睡眠、磁盘、网络、时区

代码语言:bash
复制
pmset -g | grep -E "^(sleep|disksleep|standby|hibernatemode)"
df -k /          # 根分区已用百分比
date "+%Z %z"    # cron 用的是系统本地时区

sleep 不等于 0,任务跑到一半机器睡过去,第二天你看到的就是一条残缺记录。要包一层:caffeinate -s python3 task.py。

步骤 5:确认任务到底登记了没有

「我以为它在跑」和「它真的登记了」是两回事:

代码语言:bash
复制
crontab -l
ls ~/Library/LaunchAgents | wc -l

cron 和 LaunchAgent 是两套东西,只改一边不生效。

步骤 6:跑一遍体检脚本(9 项)

完整脚本放文末附录,也可以直接用同目录 unattended_probe.py(9 项,零第三方依赖,Python 3.9+):

代码语言:bash
复制
python3 unattended_probe.py --check
python3 unattended_probe.py --check --job daily-report   # 确认目标任务登记没
python3 unattended_probe.py --check --cmd ffmpeg --cmd ollama
python3 unattended_probe.py --demo                        # 对照组,强烈建议先看这个
python3 unattended_probe.py --check --json                # 机器可读

步骤 7:用 --demo 看一次对照组

这一步是我写这个脚本最大的收获。同一个任务、两种写法、两种 PATH,跑 4 次:

代码语言:bash
复制
python3 unattended_probe.py --demo

naive 写法(ffmpeg ... | head -1 || echo "找不到")在 cron 环境下:退出码 0,没有任何报错,但 ffmpeg 那一步压根没执行,产物是残缺的,你还以为成功了。

strict 写法(先 command -v 探一次,再决定跑不跑)同一环境:退出码 1,立刻暴露。

步骤 8:把体检挂进流水线做门禁

脚本有 FAIL 项就退出码 1,可以直接前置:

代码语言:bash
复制
python3 unattended_probe.py --check --job daily-report || {
  echo "环境中检未通过,任务终止"; exit 1;
}

步骤 9(可选):真卡住了再上远程控制

前面 8 步能挡掉绝大多数。剩下的「必须人点一下」的活儿(登录态过期、授权弹窗、验证码),才是远程控制的用武之地——它是兜底,不是替代体检。

四、实测数据

以下全部为本机真实执行结果(macOS 27.0.1 / Python 3.14.3,2026-10-29 跑),路径中的用户名已替换为 ~:

检查项

级别

实测值

PATH(cron 环境)

FAIL

python3/git 在 /usr/bin 下可用;ffmpeg、ollama、node 在 cron 默认 PATH 下全部找不到

TTY(交互提示)

OK

isatty=False(已是无人值守环境)

sudo 免密

FAIL

sudo -n true 返回 1:sudo: a password is required

睡眠/休眠

OK

sleep=0、disksleep=0、standby=0、hibernatemode=3

磁盘空间

OK

根分区已用 2%

调度器登记

WARN

cron 条目 0 条,LaunchAgents 13 个(未指定 --job,只统计)

系统权限(TCC)

WARN

两个常见路径下均未找到可读的 TCC.db,无法自动确认

网络出口

OK

cloud.tencent.com:443 连通,92 ms

时区

OK

Python 视为 CST (UTC+08:00),系统 date 为 CST +0800,一致

汇总:9 项,OK 5 / WARN 2 / FAIL 2,退出码 1。

--demo 对照跑 4 次的真实结果:

写法

环境

退出码

ffmpeg 那一步

naive

当前 PATH

0

执行,输出 ffmpeg version 7.1.1

naive

cron 默认 PATH

0

静默跳过,无任何报错

strict

当前 PATH

0

执行

strict

cron 默认 PATH

1

显式报 ffmpeg 找不到

那个 naive + cron = 退出码 0 但没干活 的格子,就是这篇文章存在的理由。

耗时:9 项体检全程约 1 秒内(主要开销是 pmset、df、crontab 三个外部命令各一次)。

五、踩坑与边界

坑 1:管道会把错误吞掉(本次最大发现)。

cmd | head -1 || echo "失败" 里的 || 判的是 head 的退出码,不是 cmd 的。cmd 不存在时 head 照样成功,|| 永远不触发。改成先 command -v 探一次,或者直接判 ${PIPESTATUS[0]}。我第一版 demo 脚本就栽在这,跑出来才发现「咦,B 组怎么没有报错」。

坑 2:探测不到 ≠ 没问题。

TCC 权限(辅助功能 / 屏幕录制 / 完全磁盘访问)我试了两个常见路径都没读到 TCC.db,脚本只能报 WARN 并提示人工核对。macOS 新版本路径会变,脚本不猜——宁可报「无法确认」,也不要报「一切正常」骗你。

坑 3:报告本身就是泄密源。

体检输出含本机用户名、LaunchAgent 文件列表。脚本默认只报 LaunchAgent 数量不报文件名,外发前还要把 $HOME 替换掉。这条是上一篇文章里学到的,这次直接写进默认行为。

坑 4:sudo -n true 必须带 -n。

不带就是真的去弹密码框了,在无人值守环境里反而制造一次卡死。

边界(什么情况下这招不好使):

  • 只测环境,不测业务。产物对不对、金额勾不勾稽得上,它管不了。
  • 只看「当下这一刻」。睡眠设置、网络、磁盘都是会变的,体检结果只在跑的那一刻成立,得挂成门禁而不是跑一次就完。
  • LaunchAgent 与 cron 的语义不同(前者随登录会话、后者不随),脚本只数数量不判断语义,别拿它当迁移依据。
  • 远程控制只能解决「需要人点一下」的中断,解决不了 PATH、sudo、睡眠这些——顺序别搞反。
  • 本篇与「环境坑体检(路径/编码/时区/权限/超时)」是两回事:那篇管脚本跑起来之后内容对不对,这篇管脚本有没有机会被跑起来。

六、小结

定时任务出问题,八成不在调度器,在「你不在电脑前时没人替它答那一句」。体检 9 项、退出码做门禁、--demo 先看一次对照组,能挡掉绝大部分。剩下真需要人出手的,再交给远程控制兜底。


截图位(发布前必须补 ≥3 张真实图)

发布信息

  • 标题:#WorkBuddy# 定时任务「没跑」和「跑一半卡住」,多半不是调度器的问题:9 个中断点体检清单(附 macOS 零依赖脚本)
  • 标签:WorkBuddy、AI 办公、效率工具
  • 来源:原创
  • 专栏:AI 与大数据
  • 板块:实用文章(普通稿,非征文)
  • 字数:1980 中文字符(≥800 ✅)
  • 红线自检:待本人 grep 复核

附录:最小可用版脚本(5 项,可直接复制运行)

完整 9 项版见 草稿/assets/unattended_probe.py。下面这版 120 行左右,结论与完整版交叉验证一致(同为 FAIL 2 项)。

代码语言:python
复制
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
unattended_probe_mini.py —— 无人值守任务中断点体检器(最小可用版,5 项)
完整版 9 项见同目录 unattended_probe.py。零第三方依赖,macOS / Python 3.9+。
用法:python3 unattended_probe_mini.py
退出码:0=无 FAIL;1=至少一项 FAIL(可直接做流水线门禁)
"""
import os, subprocess, sys, time

CRON_PATH = "/usr/bin:/bin"          # macOS 上 cron 的默认 PATH
CHECKS = ["python3", "git", "curl", "ffmpeg", "ollama", "node"]

def run(cmd, timeout=5.0, env=None):
    try:
        p = subprocess.run(cmd, shell=True, capture_output=True,
                           text=True, timeout=timeout, env=env)
        return p.returncode, (p.stdout + p.stderr).strip()
    except subprocess.TimeoutExpired:
        return 124, "TIMEOUT"
    except Exception as e:                      # noqa: BLE001
        return 127, "%s: %s" % (type(e).__name__, e)

def which(cmd, path):
    rc, out = run("command -v " + cmd, env={"PATH": path})
    return out if rc == 0 and out else None

def main():
    res = []

    # 1 PATH:当前找得到、cron 找不到的命令
    cur = os.environ.get("PATH", "")
    missing = [c for c in CHECKS if which(c, cur) and not which(c, CRON_PATH)]
    res.append(("PATH(cron)", "FAIL" if missing else "OK",
                ("cron 下找不到:" + ", ".join(missing)) if missing
                else "%d 个命令在 %s 下均可解析" % (len(CHECKS), CRON_PATH)))

    # 2 TTY:无人值守环境下任何交互提示都会卡死
    atty = sys.stdin.isatty()
    res.append(("TTY", "WARN" if atty else "OK",
                "isatty=%s(True=有人盯,False=无人值守)" % atty))

    # 3 sudo 免密:无人值守下必然卡在密码提示
    rc, out = run("sudo -n true")
    res.append(("sudo免密", "OK" if rc == 0 else "FAIL",
                "sudo -n true rc=%d %s" % (rc, "" if rc == 0 else out[:40])))

    # 4 睡眠:sleep 非 0 就可能在任务中途睡过去
    rc, out = run("pmset -g")
    kv = {}
    for line in out.splitlines():
        p = line.split()
        if len(p) >= 2:
            kv[p[0]] = p[1]
    try:
        mins = int(kv.get("sleep", "-1"))
        lvl, val = ("OK", "sleep=0(不睡)") if mins == 0 else ("WARN", "sleep=%s 分钟" % mins)
    except ValueError:
        lvl, val = "WARN", "sleep=%s(非数值)" % kv.get("sleep", "?")
    res.append(("睡眠/休眠", lvl, val))

    # 5 磁盘:产出写不进去,任务会在最后一步失败
    rc, out = run("df -k /")
    tail = out.splitlines()[-1].split()
    try:
        pct = int(tail[4].rstrip("%"))
    except (ValueError, IndexError):
        pct = -1
    res.append(("磁盘空间",
                "FAIL" if pct >= 90 else ("WARN" if pct < 0 else "OK"),
                "根分区已用 %s%%" % pct))

    print("%-14s %-6s %s" % ("检查项", "级别", "实测值"))
    print("-" * 60)
    for name, lvl, val in res:
        print("%-14s %-6s %s" % (name, lvl, val))
    print("-" * 60)
    nf = sum(1 for r in res if r[1] == "FAIL")
    print("FAIL %d 项" % nf)
    return 1 if nf else 0

if __name__ == "__main__":
    sys.exit(main())

本机实测输出(最小可用版):

代码语言:plaintext
复制
检查项            级别     实测值
------------------------------------------------------------
PATH(cron)     FAIL   cron 下找不到:ffmpeg, ollama, node
TTY            OK     isatty=False(True=有人盯,False=无人值守)
sudo免密         FAIL   sudo -n true rc=1 sudo: a password is required
睡眠/休眠          OK     sleep=0(不睡)
磁盘空间           OK     根分区已用 2%
------------------------------------------------------------
FAIL 2 项

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • #WorkBuddy# 定时任务「没跑」和「跑一半卡住」,多半不是调度器的问题:9 个中断点体检清单(附 macOS 零依赖脚本)
    • 一、你会遇到的场景
    • 二、结论先行
    • 三、完整操作步骤(macOS)
      • 步骤 1:先分清是「没跑」还是「跑一半」
      • 步骤 2:体检 PATH——最隐蔽的一刀
      • 步骤 3:体检交互提示——TTY 和 sudo
      • 步骤 4:体检睡眠、磁盘、网络、时区
      • 步骤 5:确认任务到底登记了没有
      • 步骤 6:跑一遍体检脚本(9 项)
      • 步骤 7:用 --demo 看一次对照组
      • 步骤 8:把体检挂进流水线做门禁
      • 步骤 9(可选):真卡住了再上远程控制
    • 四、实测数据
    • 五、踩坑与边界
    • 六、小结
    • 截图位(发布前必须补 ≥3 张真实图)
    • 发布信息
    • 附录:最小可用版脚本(5 项,可直接复制运行)
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档