kittynode_core/application/
start_docker_if_needed.rs1use 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
46fn 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
83pub 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}