kittynode_core/application/
start_docker_if_needed.rs

1use crate::application::is_docker_running;
2use crate::application::start_docker;
3use crate::domain::operational_state::OperationalMode;
4use crate::infra::config::ConfigStore;
5use eyre::Result;
6use std::sync::{LazyLock, Mutex};
7use tracing::info;
8
9#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum DockerStartStatus {
12    Running,
13    Disabled,
14    AlreadyStarted,
15    Starting,
16}
17
18impl DockerStartStatus {
19    #[must_use]
20    pub fn as_str(self) -> &'static str {
21        match self {
22            DockerStartStatus::Running => "running",
23            DockerStartStatus::Disabled => "disabled",
24            DockerStartStatus::AlreadyStarted => "already_started",
25            DockerStartStatus::Starting => "starting",
26        }
27    }
28}
29
30static DOCKER_AUTO_STARTED: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(false));
31
32#[derive(Debug, PartialEq, Eq)]
33struct AutoStartEvaluation {
34    status: DockerStartStatus,
35    attempt_state: AttemptState,
36    invoke_start: bool,
37}
38
39#[derive(Debug, PartialEq, Eq)]
40enum AttemptState {
41    Unchanged,
42    Reset,
43    MarkAttempted,
44}
45
46// Encapsulates the branching logic for starting Docker while keeping the outer async function slim and testable.
47fn evaluate_local_auto_start(
48    auto_start_enabled: bool,
49    docker_running: bool,
50    auto_start_attempted: bool,
51) -> AutoStartEvaluation {
52    if docker_running {
53        return AutoStartEvaluation {
54            status: DockerStartStatus::Running,
55            attempt_state: AttemptState::Reset,
56            invoke_start: false,
57        };
58    }
59
60    if !auto_start_enabled {
61        return AutoStartEvaluation {
62            status: DockerStartStatus::Disabled,
63            attempt_state: AttemptState::Unchanged,
64            invoke_start: false,
65        };
66    }
67
68    if auto_start_attempted {
69        return AutoStartEvaluation {
70            status: DockerStartStatus::AlreadyStarted,
71            attempt_state: AttemptState::Unchanged,
72            invoke_start: false,
73        };
74    }
75
76    AutoStartEvaluation {
77        status: DockerStartStatus::Starting,
78        attempt_state: AttemptState::MarkAttempted,
79        invoke_start: true,
80    }
81}
82
83/// Attempts to start Docker if the configuration allows auto start and Docker is not already running.
84pub async fn start_docker_if_needed() -> Result<DockerStartStatus> {
85    let config = ConfigStore::load()?;
86    let mode = if config.server_url.trim().is_empty() {
87        OperationalMode::Local
88    } else {
89        OperationalMode::Remote
90    };
91
92    if matches!(mode, OperationalMode::Remote) {
93        return Ok(DockerStartStatus::Running);
94    }
95
96    let docker_running = is_docker_running().await;
97    let evaluation = {
98        let mut attempted = DOCKER_AUTO_STARTED
99            .lock()
100            .expect("docker auto-start mutex poisoned");
101
102        let evaluation =
103            evaluate_local_auto_start(config.auto_start_docker, docker_running, *attempted);
104
105        match evaluation.attempt_state {
106            AttemptState::Reset => *attempted = false,
107            AttemptState::MarkAttempted => *attempted = true,
108            AttemptState::Unchanged => {}
109        }
110
111        if !evaluation.invoke_start {
112            return Ok(evaluation.status);
113        }
114
115        drop(attempted);
116        evaluation
117    };
118
119    info!("Starting Docker Desktop via auto-start preference");
120    match start_docker().await {
121        Ok(()) => Ok(evaluation.status),
122        Err(err) => {
123            let mut attempted = DOCKER_AUTO_STARTED
124                .lock()
125                .expect("docker auto-start mutex poisoned");
126            *attempted = false;
127            Err(err)
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::evaluate_local_auto_start;
135    use super::{AttemptState, AutoStartEvaluation, DockerStartStatus};
136
137    #[test]
138    fn running_docker_is_reported_and_resets_attempt() {
139        let evaluation = evaluate_local_auto_start(true, true, true);
140
141        assert_eq!(
142            evaluation,
143            AutoStartEvaluation {
144                status: DockerStartStatus::Running,
145                attempt_state: AttemptState::Reset,
146                invoke_start: false,
147            }
148        );
149    }
150
151    #[test]
152    fn disabled_auto_start_reflects_disabled_status() {
153        let evaluation = evaluate_local_auto_start(false, false, false);
154
155        assert_eq!(
156            evaluation,
157            AutoStartEvaluation {
158                status: DockerStartStatus::Disabled,
159                attempt_state: AttemptState::Unchanged,
160                invoke_start: false,
161            }
162        );
163    }
164
165    #[test]
166    fn repeated_attempt_returns_already_started_without_changes() {
167        let evaluation = evaluate_local_auto_start(true, false, true);
168
169        assert_eq!(
170            evaluation,
171            AutoStartEvaluation {
172                status: DockerStartStatus::AlreadyStarted,
173                attempt_state: AttemptState::Unchanged,
174                invoke_start: false,
175            }
176        );
177    }
178
179    #[test]
180    fn first_attempt_requests_docker_start_and_marks_attempt() {
181        let evaluation = evaluate_local_auto_start(true, false, false);
182
183        assert_eq!(
184            evaluation,
185            AutoStartEvaluation {
186                status: DockerStartStatus::Starting,
187                attempt_state: AttemptState::MarkAttempted,
188                invoke_start: true,
189            }
190        );
191    }
192
193    #[test]
194    fn docker_start_status_strings_match_expected_variants() {
195        assert_eq!(DockerStartStatus::Running.as_str(), "running");
196        assert_eq!(DockerStartStatus::Disabled.as_str(), "disabled");
197        assert_eq!(
198            DockerStartStatus::AlreadyStarted.as_str(),
199            "already_started"
200        );
201        assert_eq!(DockerStartStatus::Starting.as_str(), "starting");
202    }
203}