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