Skip to main content

client_core/container/
environment.rs

1//! 环境检测模块
2//!
3//! 用于检测当前运行环境,包括操作系统、路径格式等,
4//! 为跨平台兼容提供支持。
5//!
6//! 注意:本项目通过 Docker API 与容器引擎通信(使用 bollard 库),
7//! 支持 Docker 和 Podman(Docker 兼容模式),因此不需要区分底层容器引擎类型。
8
9use std::env;
10use std::sync::OnceLock;
11use tokio::process::Command;
12use tracing::{debug, info, warn};
13
14/// 全局存储检测到的 Docker Compose 命令类型
15static COMPOSE_COMMAND_TYPE: OnceLock<ComposeCommandType> = OnceLock::new();
16
17/// Docker Compose 命令类型
18#[derive(Debug, Clone, Copy, PartialEq, Default)]
19pub enum ComposeCommandType {
20    /// 使用 docker compose 子命令(Docker 20.10.13+)
21    DockerComposeSubcommand,
22    /// 使用独立的 docker-compose 命令
23    DockerComposeStandalone,
24    /// 未检测(默认值)
25    #[default]
26    Unknown,
27}
28
29/// 检测 docker compose 命令类型(执行命令检测)
30pub async fn detect_compose_command_type() -> ComposeCommandType {
31    info!("🔍 Detecting Docker Compose command type...");
32
33    // 1. 尝试 docker compose version(新语法)
34    let output = Command::new("docker")
35        .args(["compose", "version"])
36        .output()
37        .await;
38
39    if let Ok(output) = output {
40        if output.status.success() {
41            let version_info = String::from_utf8_lossy(&output.stdout);
42            info!(
43                "   ✅ Using docker compose subcommand: {}",
44                version_info.trim()
45            );
46            return ComposeCommandType::DockerComposeSubcommand;
47        }
48        debug!(
49            "   docker compose version returned non-zero status: {:?}",
50            output.status
51        );
52    }
53
54    // 2. 回退到 docker-compose --version(旧语法)
55    debug!("   Trying standalone docker-compose command...");
56    let output = Command::new("docker-compose")
57        .arg("--version")
58        .output()
59        .await;
60
61    if let Ok(output) = output {
62        if output.status.success() {
63            let version_info = String::from_utf8_lossy(&output.stdout);
64            info!(
65                "   ✅ Using standalone docker-compose command: {}",
66                version_info.trim()
67            );
68            return ComposeCommandType::DockerComposeStandalone;
69        }
70    }
71
72    warn!("   ⚠️ No available Docker Compose command detected");
73    ComposeCommandType::Unknown
74}
75
76/// 设置全局 Docker Compose 命令类型(仅能设置一次)
77///
78/// 在命令入口处(如 main.rs)调用 detect_compose_command_type() 后,
79/// 使用此函数存储检测结果,后续无需再次检测。
80pub fn set_compose_command_type(compose_type: ComposeCommandType) {
81    if COMPOSE_COMMAND_TYPE.set(compose_type).is_err() {
82        debug!("Compose command type already set; ignoring duplicate initialization");
83    }
84}
85
86/// 获取已检测的 Docker Compose 命令类型
87///
88/// 返回已检测的命令类型,如果未检测则返回 Unknown
89pub fn get_compose_command_type() -> ComposeCommandType {
90    COMPOSE_COMMAND_TYPE
91        .get()
92        .copied()
93        .unwrap_or(ComposeCommandType::Unknown)
94}
95
96/// 主机操作系统类型
97#[derive(Debug, Clone, PartialEq)]
98pub enum HostOs {
99    /// Windows + WSL2 环境
100    WindowsWsl2,
101    /// 原生 Windows 环境
102    WindowsNative,
103    /// 原生 Linux 环境
104    LinuxNative,
105    /// macOS 环境
106    MacOs,
107}
108
109impl HostOs {
110    /// 获取显示名称
111    pub fn display_name(&self) -> &'static str {
112        match self {
113            HostOs::WindowsWsl2 => "Windows (WSL2)",
114            HostOs::WindowsNative => "Windows (Native)",
115            HostOs::LinuxNative => "Linux",
116            HostOs::MacOs => "macOS",
117        }
118    }
119
120    /// 检查是否为 Windows 环境(包括 WSL2 和原生)
121    pub fn is_windows(&self) -> bool {
122        matches!(self, HostOs::WindowsWsl2 | HostOs::WindowsNative)
123    }
124
125    /// 检查是否为 WSL2 环境
126    pub fn is_wsl2(&self) -> bool {
127        matches!(self, HostOs::WindowsWsl2)
128    }
129
130    /// 检查是否需要提前创建挂载目录
131    ///
132    /// Windows 环境下(包括 WSL2 和原生 Windows),容器引擎不会自动创建
133    /// docker-compose.yml 中定义的挂载目录,需要提前手动创建。
134    pub fn needs_early_mount_check(&self) -> bool {
135        self.is_windows()
136    }
137}
138
139/// 路径格式类型
140#[derive(Debug, Clone, PartialEq)]
141pub enum PathFormat {
142    /// WSL2 格式:/mnt/c/...
143    Wsl2,
144    /// Windows 格式:C:\...
145    Windows,
146    /// POSIX 格式:/...
147    Posix,
148}
149
150impl PathFormat {
151    pub fn display_name(&self) -> &'static str {
152        match self {
153            PathFormat::Wsl2 => "WSL2",
154            PathFormat::Windows => "Windows",
155            PathFormat::Posix => "POSIX",
156        }
157    }
158}
159
160/// 运行时环境信息
161#[derive(Debug, Clone)]
162pub struct RuntimeEnvironment {
163    pub host_os: HostOs,
164    pub path_format: PathFormat,
165}
166
167impl RuntimeEnvironment {
168    /// 获取环境摘要信息
169    pub fn summary(&self) -> String {
170        format!(
171            "{} ({})",
172            self.host_os.display_name(),
173            self.path_format.display_name()
174        )
175    }
176
177    /// 检查是否需要特殊处理
178    ///
179    /// 在 Windows 环境下(包括 WSL2 和原生 Windows),容器引擎
180    /// (Docker Desktop 或 Podman Desktop)都不会自动创建
181    /// docker-compose.yml 中定义的挂载目录,因此需要提前主动创建这些目录。
182    ///
183    /// 在 Linux/macOS 环境下,系统会自动创建挂载目录,无需特殊处理。
184    pub fn needs_special_handling(&self) -> bool {
185        self.host_os.is_windows()
186    }
187
188    /// 检查是否为 WSL2 环境
189    pub fn is_wsl2(&self) -> bool {
190        self.host_os.is_wsl2()
191    }
192}
193
194/// 检测当前运行环境
195pub fn detect_runtime_environment() -> RuntimeEnvironment {
196    debug!("🔍 Detecting runtime environment...");
197
198    // 检测主机操作系统
199    let host_os = detect_host_os();
200    debug!("   Host OS: {:?}", host_os);
201
202    // 检测路径格式
203    let path_format = detect_path_format(&host_os);
204    debug!("   Path format: {:?}", path_format);
205
206    let env = RuntimeEnvironment {
207        host_os,
208        path_format,
209    };
210
211    info!("✅ Runtime environment detected: {}", env.summary());
212
213    if env.needs_special_handling() {
214        info!("⚠️ Windows environment detected; mount directories should be created early");
215    }
216
217    env
218}
219
220/// 检测主机操作系统
221fn detect_host_os() -> HostOs {
222    // 检查是否为 WSL2
223    if is_running_in_wsl() {
224        return HostOs::WindowsWsl2;
225    }
226
227    // 检测原生操作系统
228    match std::env::consts::OS {
229        "windows" => HostOs::WindowsNative,
230        "linux" => HostOs::LinuxNative,
231        "macos" => HostOs::MacOs,
232        other => {
233            debug!("Unknown operating system: {}, assuming Linux", other);
234            HostOs::LinuxNative
235        }
236    }
237}
238
239/// 检测是否在 WSL2 中运行
240fn is_running_in_wsl() -> bool {
241    // 方法 1: 检查 /proc/version
242    if let Ok(version) = std::fs::read_to_string("/proc/version") {
243        if version.to_lowercase().contains("microsoft") {
244            debug!("Detected WSL marker in /proc/version");
245            return true;
246        }
247    }
248
249    // 方法 2: 检查 WSL 环境变量
250    if env::var("WSL_DISTRO_NAME").is_ok() {
251        debug!("Detected WSL_DISTRO_NAME environment variable");
252        return true;
253    }
254
255    if env::var("WSLENV").is_ok() {
256        debug!("Detected WSLENV environment variable");
257        return true;
258    }
259
260    false
261}
262
263/// 检测路径格式
264fn detect_path_format(host_os: &HostOs) -> PathFormat {
265    match host_os {
266        HostOs::WindowsWsl2 => PathFormat::Wsl2,
267        HostOs::WindowsNative => PathFormat::Windows,
268        HostOs::LinuxNative | HostOs::MacOs => PathFormat::Posix,
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_host_os_display_name() {
278        assert_eq!(HostOs::WindowsWsl2.display_name(), "Windows (WSL2)");
279        assert_eq!(HostOs::WindowsNative.display_name(), "Windows (Native)");
280        assert_eq!(HostOs::LinuxNative.display_name(), "Linux");
281        assert_eq!(HostOs::MacOs.display_name(), "macOS");
282    }
283
284    #[test]
285    fn test_host_os_is_windows() {
286        assert!(HostOs::WindowsWsl2.is_windows());
287        assert!(HostOs::WindowsNative.is_windows());
288        assert!(!HostOs::LinuxNative.is_windows());
289        assert!(!HostOs::MacOs.is_windows());
290    }
291
292    #[test]
293    fn test_host_os_needs_early_mount_check() {
294        assert!(HostOs::WindowsWsl2.needs_early_mount_check());
295        assert!(HostOs::WindowsNative.needs_early_mount_check());
296        assert!(!HostOs::LinuxNative.needs_early_mount_check());
297        assert!(!HostOs::MacOs.needs_early_mount_check());
298    }
299
300    #[test]
301    fn test_path_format_display_name() {
302        assert_eq!(PathFormat::Wsl2.display_name(), "WSL2");
303        assert_eq!(PathFormat::Windows.display_name(), "Windows");
304        assert_eq!(PathFormat::Posix.display_name(), "POSIX");
305    }
306
307    #[test]
308    fn test_runtime_environment_summary() {
309        let env = RuntimeEnvironment {
310            host_os: HostOs::WindowsWsl2,
311            path_format: PathFormat::Wsl2,
312        };
313
314        assert_eq!(env.summary(), "Windows (WSL2) (WSL2)");
315    }
316
317    #[test]
318    fn test_runtime_environment_is_wsl2() {
319        let env_wsl2 = RuntimeEnvironment {
320            host_os: HostOs::WindowsWsl2,
321            path_format: PathFormat::Wsl2,
322        };
323        assert!(env_wsl2.is_wsl2());
324
325        let env_linux = RuntimeEnvironment {
326            host_os: HostOs::LinuxNative,
327            path_format: PathFormat::Posix,
328        };
329        assert!(!env_linux.is_wsl2());
330    }
331
332    #[test]
333    fn test_runtime_environment_needs_special_handling() {
334        // Windows WSL2 环境 → 需要特殊处理
335        let env_wsl2 = RuntimeEnvironment {
336            host_os: HostOs::WindowsWsl2,
337            path_format: PathFormat::Wsl2,
338        };
339        assert!(env_wsl2.needs_special_handling());
340
341        // Windows 原生环境 → 需要特殊处理
342        let env_windows_native = RuntimeEnvironment {
343            host_os: HostOs::WindowsNative,
344            path_format: PathFormat::Windows,
345        };
346        assert!(env_windows_native.needs_special_handling());
347
348        // Linux 环境 → 不需要特殊处理
349        let env_linux = RuntimeEnvironment {
350            host_os: HostOs::LinuxNative,
351            path_format: PathFormat::Posix,
352        };
353        assert!(!env_linux.needs_special_handling());
354
355        // macOS 环境 → 不需要特殊处理
356        let env_macos = RuntimeEnvironment {
357            host_os: HostOs::MacOs,
358            path_format: PathFormat::Posix,
359        };
360        assert!(!env_macos.needs_special_handling());
361    }
362
363    #[test]
364    fn test_compose_command_type_default() {
365        assert_eq!(ComposeCommandType::default(), ComposeCommandType::Unknown);
366    }
367
368    #[test]
369    fn test_compose_command_type_equality() {
370        assert_eq!(
371            ComposeCommandType::DockerComposeSubcommand,
372            ComposeCommandType::DockerComposeSubcommand
373        );
374        assert_eq!(
375            ComposeCommandType::DockerComposeStandalone,
376            ComposeCommandType::DockerComposeStandalone
377        );
378        assert_eq!(ComposeCommandType::Unknown, ComposeCommandType::Unknown);
379
380        assert_ne!(
381            ComposeCommandType::DockerComposeSubcommand,
382            ComposeCommandType::DockerComposeStandalone
383        );
384        assert_ne!(
385            ComposeCommandType::DockerComposeSubcommand,
386            ComposeCommandType::Unknown
387        );
388    }
389
390    #[test]
391    fn test_compose_command_type_clone_copy() {
392        let original = ComposeCommandType::DockerComposeSubcommand;
393        let cloned = original.clone();
394        let copied = original;
395
396        assert_eq!(original, cloned);
397        assert_eq!(original, copied);
398    }
399
400    #[test]
401    fn test_compose_command_type_debug() {
402        // 测试 Debug trait 实现
403        let subcommand = ComposeCommandType::DockerComposeSubcommand;
404        let standalone = ComposeCommandType::DockerComposeStandalone;
405        let unknown = ComposeCommandType::Unknown;
406
407        assert_eq!(format!("{:?}", subcommand), "DockerComposeSubcommand");
408        assert_eq!(format!("{:?}", standalone), "DockerComposeStandalone");
409        assert_eq!(format!("{:?}", unknown), "Unknown");
410    }
411}