Skip to main content

yggdrasil/api/code_runner/
readiness.rs

1//! 启动期代码运行器就绪探测(server-only)。
2//!
3//! 博客本身不依赖 Docker——代码运行是可选功能。本模块在启动后台 best-effort 探测
4//! Docker daemon 是否可达、已启用的运行器镜像是否已构建,缺失时打印可操作日志,
5//! **不阻塞启动、不 exit**。让 issue #20 那类「socket 其实通了但镜像没构建」的失败
6//! 从完全不可见变得可见且可操作。
7//!
8//! 不自动构建镜像、不新增运行时依赖(用户已选「诊断 + 改善错误信息」)。
9
10#![cfg(feature = "server")]
11
12use std::collections::HashSet;
13
14use bollard::query_parameters::ListImagesOptions;
15
16use crate::api::code_runner::languages::{is_supported_lang, LANGUAGES};
17use crate::infra::docker::DOCKER_CLIENT;
18
19/// 启动后台调用:探测 Docker daemon 与已启用运行器镜像的就绪度,缺失时打日志。
20///
21/// 全程 best-effort:任何一步失败只记日志后 return,绝不 panic、不阻塞启动。
22/// 尊重 `CODE_RUNNER_LANGUAGES` 白名单——只探测 [`is_supported_lang`] 为真的语言,
23/// 不为运维收窄掉的镜像报「缺失」。
24pub async fn log_runner_readiness() {
25    let docker = match DOCKER_CLIENT.as_ref() {
26        Some(d) => d,
27        None => {
28            tracing::warn!(
29                "代码运行器已禁用:Docker daemon 未连接。本地开发需运行 Docker 并挂载 socket\
30                 (dev 容器见 docker-compose.dev.yml),或设置 DOCKER_SOCKET_PATH"
31            );
32            return;
33        }
34    };
35
36    // 探活 daemon:version 仅用于判断「daemon 是否响应」,失败即 warn 后 return。
37    if let Err(e) = docker.version().await {
38        tracing::warn!("Docker daemon 可达性检查失败:{e},代码运行可能不可用");
39        return;
40    }
41
42    // 列举本机镜像 tag 集合(repo_tags 是 Vec<String>,可能含多个 tag)。
43    let images = match docker.list_images(None::<ListImagesOptions>).await {
44        Ok(v) => v,
45        Err(e) => {
46            tracing::warn!("Docker 镜像列表查询失败:{e},跳过运行器镜像就绪检查");
47            return;
48        }
49    };
50    let tags: HashSet<String> = images
51        .iter()
52        .flat_map(|i| i.repo_tags.iter().cloned())
53        .collect();
54
55    // 仅探测已启用(命中白名单)的语言镜像,避免为运维收窄掉的语言报缺失。
56    let registered: Vec<(&String, &str)> = LANGUAGES
57        .iter()
58        .filter(|(k, _)| is_supported_lang(k))
59        .map(|(k, def)| (k, def.image.as_str()))
60        .collect();
61
62    if registered.is_empty() {
63        tracing::warn!("无已注册运行器语言");
64        return;
65    }
66
67    let missing: Vec<&str> = registered
68        .iter()
69        .filter_map(|(_, img)| {
70            if tags.contains(*img) {
71                None
72            } else {
73                Some(*img)
74            }
75        })
76        .collect();
77
78    let lang_names: Vec<&str> = registered.iter().map(|(k, _)| k.as_str()).collect();
79
80    if missing.is_empty() {
81        tracing::info!(
82            "代码运行器就绪:{}/{} 镜像已构建({})",
83            registered.len(),
84            registered.len(),
85            lang_names.join("/")
86        );
87    } else {
88        tracing::warn!(
89            "代码运行器镜像未构建:{}。请在宿主执行 `bash docker/build-runners.sh` 构建后再使用代码运行功能",
90            missing.join("、")
91        );
92    }
93}