1use std::path::Path;
4
5use vissue_core::config::Layout;
6use vissue_serve::ServeConfig;
7
8use crate::backend::BoardBackend;
9use crate::core_backend::CoreBackend;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ServeStatus {
14 Live,
16 Offline,
18 Mismatch,
20}
21
22pub type ProbeFn = fn(&Path) -> bool;
24pub type EnsureFn = fn(&ServeConfig) -> Result<vissue_serve::EnsureResult, String>;
26pub type ConnectFn = fn(&Path, &Layout, &str) -> Result<Box<dyn BoardBackend>, AttachFail>;
28
29#[derive(Debug)]
31pub struct AttachHooks {
32 pub probe: ProbeFn,
34 pub ensure: EnsureFn,
36 pub connect: ConnectFn,
38}
39
40#[derive(Debug)]
42pub enum AttachFail {
43 Mismatch(String),
45 Other(String),
47}
48
49impl Default for AttachHooks {
50 fn default() -> Self {
51 Self {
52 probe: default_probe,
53 ensure: default_ensure,
54 connect: default_connect,
55 }
56 }
57}
58
59fn default_probe(path: &Path) -> bool {
60 vissue_serve::socket_accepts(path)
61}
62
63fn default_ensure(cfg: &ServeConfig) -> Result<vissue_serve::EnsureResult, String> {
64 vissue_serve::ensure_serve(cfg).map_err(|e| e.to_string())
65}
66
67fn default_connect(
68 path: &Path,
69 layout: &Layout,
70 agent: &str,
71) -> Result<Box<dyn BoardBackend>, AttachFail> {
72 #[cfg(unix)]
73 {
74 use crate::control::{ControlAttachError, ControlBackend};
75 match ControlBackend::connect(path, layout, agent) {
76 Ok(backend) => Ok(Box::new(backend)),
77 Err(ControlAttachError::Mismatch {
78 want_root,
79 want_prefix,
80 got_root,
81 got_prefix,
82 }) => Err(AttachFail::Mismatch(format!(
83 "want {want_root}/{want_prefix} got {got_root}/{got_prefix}"
84 ))),
85 Err(err) => Err(AttachFail::Other(err.to_string())),
86 }
87 }
88 #[cfg(not(unix))]
89 {
90 let _ = (path, layout, agent);
91 Err(AttachFail::Other("vissue tui attach is Unix-only".into()))
92 }
93}
94
95#[derive(Debug)]
98pub enum AttachOutcome {
99 Stay {
101 status: ServeStatus,
103 message: String,
105 },
106 Switch {
108 backend: Box<dyn BoardBackend>,
110 status: ServeStatus,
112 },
113}
114
115pub fn try_attach(
118 layout: &Layout,
119 socket: &Path,
120 agent: &str,
121 offline: bool,
122 hooks: &AttachHooks,
123) -> AttachOutcome {
124 if offline {
125 return AttachOutcome::Stay {
126 status: ServeStatus::Offline,
127 message: String::new(),
128 };
129 }
130
131 if (hooks.probe)(socket) {
132 return finish_connect(socket, layout, agent, hooks);
133 }
134
135 let cfg = ServeConfig {
136 layout: layout.clone(),
137 socket: socket.to_path_buf(),
138 exe: None,
139 };
140 match (hooks.ensure)(&cfg) {
141 Ok(ensured) if ensured.ok && (hooks.probe)(socket) => {
142 finish_connect(socket, layout, agent, hooks)
143 }
144 Ok(ensured) => AttachOutcome::Stay {
145 status: ServeStatus::Offline,
146 message: ensured.error.unwrap_or_else(|| "serve spawn failed".into()),
147 },
148 Err(err) => AttachOutcome::Stay {
149 status: ServeStatus::Offline,
150 message: err,
151 },
152 }
153}
154
155fn finish_connect(
156 socket: &Path,
157 layout: &Layout,
158 agent: &str,
159 hooks: &AttachHooks,
160) -> AttachOutcome {
161 match (hooks.connect)(socket, layout, agent) {
162 Ok(backend) => AttachOutcome::Switch {
163 backend,
164 status: ServeStatus::Live,
165 },
166 Err(AttachFail::Mismatch(message)) => AttachOutcome::Stay {
167 status: ServeStatus::Mismatch,
168 message,
169 },
170 Err(AttachFail::Other(message)) => AttachOutcome::Stay {
171 status: ServeStatus::Offline,
172 message,
173 },
174 }
175}
176
177pub fn open_core(layout: Layout, agent: String) -> Result<CoreBackend, vissue_core::error::Error> {
183 CoreBackend::open(layout, agent)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::backend::BoardBackend;
190 use std::path::{Path, PathBuf};
191 use std::sync::atomic::{AtomicBool, Ordering};
192
193 static TOUCHED: AtomicBool = AtomicBool::new(false);
194
195 fn panic_probe(_: &Path) -> bool {
196 TOUCHED.store(true, Ordering::SeqCst);
197 panic!("--offline must not probe the socket");
198 }
199
200 fn panic_ensure(_: &ServeConfig) -> Result<vissue_serve::EnsureResult, String> {
201 TOUCHED.store(true, Ordering::SeqCst);
202 panic!("--offline must not spawn serve");
203 }
204
205 fn panic_connect(_: &Path, _: &Layout, _: &str) -> Result<Box<dyn BoardBackend>, AttachFail> {
206 TOUCHED.store(true, Ordering::SeqCst);
207 panic!("--offline must not connect");
208 }
209
210 fn no_probe(_: &Path) -> bool {
211 false
212 }
213
214 fn ensure_fails(_: &ServeConfig) -> Result<vissue_serve::EnsureResult, String> {
215 Err("spawn failed".into())
216 }
217
218 fn connect_mismatch(
219 _: &Path,
220 _: &Layout,
221 _: &str,
222 ) -> Result<Box<dyn BoardBackend>, AttachFail> {
223 Err(AttachFail::Mismatch("other root".into()))
224 }
225
226 fn yes_probe(_: &Path) -> bool {
227 true
228 }
229
230 #[test]
231 fn spawn_failure_stays_core() {
232 let layout = Layout::new("/tmp/vissue-spawn", "Software");
233 let hooks = AttachHooks {
234 probe: no_probe,
235 ensure: ensure_fails,
236 connect: panic_connect,
237 };
238 match try_attach(
239 &layout,
240 &PathBuf::from("/tmp/vissue-spawn.sock"),
241 "agent",
242 false,
243 &hooks,
244 ) {
245 AttachOutcome::Stay {
246 status: ServeStatus::Offline,
247 message,
248 } => assert!(message.contains("spawn failed"), "{message}"),
249 _ => panic!("expected offline stay"),
250 }
251 }
252
253 #[test]
254 fn mismatch_stays_core() {
255 let layout = Layout::new("/tmp/vissue-mis", "Software");
256 let hooks = AttachHooks {
257 probe: yes_probe,
258 ensure: panic_ensure,
259 connect: connect_mismatch,
260 };
261 match try_attach(
262 &layout,
263 &PathBuf::from("/tmp/vissue-mis.sock"),
264 "agent",
265 false,
266 &hooks,
267 ) {
268 AttachOutcome::Stay {
269 status: ServeStatus::Mismatch,
270 message,
271 } => assert!(message.contains("other root"), "{message}"),
272 _ => panic!("expected mismatch stay"),
273 }
274 }
275
276 #[test]
277 fn offline_never_connects() {
278 TOUCHED.store(false, Ordering::SeqCst);
279 let layout = Layout::new("/tmp/vissue-offline", "Software");
280 let hooks = AttachHooks {
281 probe: panic_probe,
282 ensure: panic_ensure,
283 connect: panic_connect,
284 };
285 let outcome = try_attach(
286 &layout,
287 &PathBuf::from("/tmp/vissue-offline.sock"),
288 "agent",
289 true,
290 &hooks,
291 );
292 match outcome {
293 AttachOutcome::Stay {
294 status: ServeStatus::Offline,
295 ..
296 } => {}
297 _ => panic!("offline must stay on CoreBackend"),
298 }
299 assert!(!TOUCHED.load(Ordering::SeqCst));
300 }
301}