多进程并发执行 tccli 时配置文件竞争导致 exit 255 / JSON 解析错误
问题简述
当多个 tccli 进程并发执行时,会同时读写 ~/.tccli/*.configure,且读写过程无文件锁、非原子写入,导致一个进程可能读到空文件或写了一半的文件,从而以 exit 255 退出,并出现如下错误:
Expecting value: line 1 column 1 (char 0)(读到空文件)
Expecting value: line 366 column 16(读到截断的 JSON)
上述错误发生在真正发起腾讯云 API 请求之前,异常内容来自本地配置文件,而非云 API 返回。
环境
- tccli:3.0.x(pip / homebrew 安装)
- 系统:Linux / macOS
- 复现方式:任意并发调用 tccli(例如 12 个
tccli vpc DescribeSecurityGroupPolicies --region ap-hongkong,或 tccli vpc DescribeRegions --region ap-guangzhou)
复现步骤
- 先配置一次 tccli:执行
tccli configure 或 tccli auth login。
- 并发执行多个 tccli 进程(例如 12 个):
for i in $(seq 1 12); do
tccli vpc DescribeRegions --region ap-guangzhou > "out_$i.json" 2> "err_$i.txt" & done
wait
# 检查退出码,部分会是 255
或使用最小复现脚本(见附件/仓库):
chmod +x repro_tccli_concurrent_config.sh
./repro_tccli_concurrent_config.sh 12 ap-guangzhou
- 现象:部分进程以 255 退出,stderr 中出现 JSON 解析错误;其余可能成功。
预期与实际
- 预期:所有调用应正常完成,或仅因 API/网络原因失败;配置文件的读写在并发下应安全。
- 实际:并发时会有一定比例进程 exit 255,解析错误指向本地配置文件(空或截断的 JSON),而非 API 响应。失败可发生在进程尚未执行到具体服务 action 之前(已通过进程内探针验证)。
根因(代码链路)
- 每次 tccli 启动都会构建
ConfigureCommand() 并调用 init_configures()。
init_configures() 最终调用 _init_configure(),对当前 profile 及所有 *.configure 文件执行 open(..., "w") 重写,无文件锁、非原子写入。
- 另一进程若在读取同一配置(例如在
tccli/loaders.py → Utils.load_json_msg() 约第 77 行),可能读到:
- 空文件 →
body_len = 0 → Expecting value: line 1 column 1 (char 0)。
- 截断文件 → 例如
body_len = 8187 → Expecting value: line 366 column 16。
相关调用链:
- 读路径:
loaders.py → Utils.load_json_msg()(约第 77 行)读取 ~/.tccli/default.configure(或当前 profile 对应文件)。
- 写路径:
command.py(约第 95 行)构建 ConfigureCommand() → configure.py(约第 412 行)init_configures() →(约第 472 行)_init_configure() →(约第 45 行)open(..., "w");另见 utils.py(约第 83 行)。
因此,“非 JSON” 来自正在被其他 tccli 进程写入的本地配置文件。
与现象的一致性
- 串行执行:多数正常;若与其他 tccli 或工具同时碰同一配置,仍可能偶发失败。
- 并发执行:失败率明显升高。
- 请求前即失败:有时失败进程从未进入具体服务的 action,与在初始加载配置阶段失败相符。
修复建议
- 方案 A:对每个
*.configure 的读、写使用文件锁(如 fcntl.flock 或 filelock),保证写时独占,读时与写互斥。
- 方案 B:原子写入:先写入同目录下的临时文件,再用
os.rename() 替换最终 *.configure 路径,避免读者看到半写状态。
任选其一(或两者同时采用)即可让并发 tccli 调用在配置读写上安全。
最小复现脚本:repro_tccli_concurrent_config.sh(并发执行 N 次 tccli cvm DescribeRegions,统计成功与 exit 255 数量并输出示例 stderr)。
#!/usr/bin/env bash
#
# Minimal repro: concurrent tccli processes can corrupt ~/.tccli/*.configure
# (no file lock, non-atomic write). Result: exit 255, "Expecting value: line 1
# column 1" or truncated JSON parse errors — before any API request.
#
# Usage:
# ./repro_tccli_concurrent_config.sh [N] [region]
# Default: N=12, region=ap-guangzhou
#
# Prereq: tccli configured (e.g. tccli configure or tccli auth login).
# Run in an environment where ~/.tccli is writable (e.g. your local terminal);
# in a sandbox that blocks ~/.tccli/log, tccli will fail with PermissionError before hitting the config race.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${SCRIPT_DIR}/repro_out"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
N="${1:-12}"
REGION="${2:-ap-guangzhou}"
# Use any tccli call that loads config; DescribeRegions needs no extra args
TCCLI_CMD="tccli cvm DescribeRegions"
echo "Repro: $N concurrent invocations of: $TCCLI_CMD"
echo "Working dir: $WORK_DIR"
echo ""
for i in $(seq 1 "$N"); do
(
$TCCLI_CMD > "out_$i.json" 2> "err_$i.txt"
echo $? > "exit_$i"
) &
done
wait
ok=0
fail=0
for i in $(seq 1 "$N"); do
code=$(cat "exit_$i" 2>/dev/null || echo -1)
if [ "$code" = "0" ]; then
((ok++)) || true
else
((fail++)) || true
echo "--- FAIL #$i (exit $code) stderr ---"
cat "err_$i.txt" 2>/dev/null || true
echo ""
fi
done
echo "Result: $ok OK, $fail FAIL (of $N)"
if [ "$fail" -gt 0 ]; then
echo "Sample failure stderr (first non-zero):"
for i in $(seq 1 "$N"); do
if [ "$(cat exit_$i)" != "0" ]; then
cat "err_$i.txt"
break
fi
done
exit 1
fi
exit 0
多进程并发执行 tccli 时配置文件竞争导致 exit 255 / JSON 解析错误
问题简述
当多个
tccli进程并发执行时,会同时读写~/.tccli/*.configure,且读写过程无文件锁、非原子写入,导致一个进程可能读到空文件或写了一半的文件,从而以 exit 255 退出,并出现如下错误:Expecting value: line 1 column 1 (char 0)(读到空文件)Expecting value: line 366 column 16(读到截断的 JSON)上述错误发生在真正发起腾讯云 API 请求之前,异常内容来自本地配置文件,而非云 API 返回。
环境
tccli vpc DescribeSecurityGroupPolicies --region ap-hongkong,或tccli vpc DescribeRegions --region ap-guangzhou)复现步骤
tccli configure或tccli auth login。或使用最小复现脚本(见附件/仓库):
预期与实际
根因(代码链路)
ConfigureCommand()并调用init_configures()。init_configures()最终调用_init_configure(),对当前 profile 及所有*.configure文件执行open(..., "w")重写,无文件锁、非原子写入。tccli/loaders.py→Utils.load_json_msg()约第 77 行),可能读到:body_len = 0→Expecting value: line 1 column 1 (char 0)。body_len = 8187→Expecting value: line 366 column 16。相关调用链:
loaders.py→Utils.load_json_msg()(约第 77 行)读取~/.tccli/default.configure(或当前 profile 对应文件)。command.py(约第 95 行)构建ConfigureCommand()→configure.py(约第 412 行)init_configures()→(约第 472 行)_init_configure()→(约第 45 行)open(..., "w");另见utils.py(约第 83 行)。因此,“非 JSON” 来自正在被其他 tccli 进程写入的本地配置文件。
与现象的一致性
修复建议
*.configure的读、写使用文件锁(如fcntl.flock或filelock),保证写时独占,读时与写互斥。os.rename()替换最终*.configure路径,避免读者看到半写状态。任选其一(或两者同时采用)即可让并发 tccli 调用在配置读写上安全。
最小复现脚本:
repro_tccli_concurrent_config.sh(并发执行 N 次tccli cvm DescribeRegions,统计成功与 exit 255 数量并输出示例 stderr)。