1#![allow(dead_code)]
2use std::collections::HashMap;
19use std::sync::Mutex;
20use std::time::Duration;
21
22use serde::{Deserialize, Serialize};
23use tokio::sync::oneshot;
24use tokio::time;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct UserInputRequest {
32 #[serde(rename = "requestid")]
34 pub request_id: String,
35
36 #[serde(rename = "querytext")]
38 pub query_text: String,
39
40 #[serde(rename = "responsetype")]
42 pub response_type: String,
43
44 pub title: String,
46
47 #[serde(default)]
49 pub markdown: bool,
50
51 #[serde(rename = "timeoutms", default)]
53 pub timeout_ms: i64,
54
55 #[serde(rename = "checkboxmsg", default, skip_serializing_if = "String::is_empty")]
57 pub checkbox_msg: String,
58
59 #[serde(rename = "publictext", default)]
61 pub public_text: bool,
62
63 #[serde(rename = "oklabel", default, skip_serializing_if = "String::is_empty")]
65 pub ok_label: String,
66
67 #[serde(rename = "cancellabel", default, skip_serializing_if = "String::is_empty")]
69 pub cancel_label: String,
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct UserInputResponse {
76 #[serde(rename = "type", default)]
78 pub response_type: String,
79
80 #[serde(rename = "requestid")]
82 pub request_id: String,
83
84 #[serde(default, skip_serializing_if = "String::is_empty")]
86 pub text: String,
87
88 #[serde(default)]
90 pub confirm: bool,
91
92 #[serde(rename = "errormsg", default, skip_serializing_if = "String::is_empty")]
94 pub error_msg: String,
95
96 #[serde(rename = "checkboxstat", default)]
98 pub checkbox_stat: bool,
99}
100
101impl UserInputResponse {
102 pub fn is_error(&self) -> bool {
104 !self.error_msg.is_empty()
105 }
106
107 pub fn is_confirmed(&self) -> bool {
109 self.confirm && self.error_msg.is_empty()
110 }
111}
112
113pub const RESPONSE_TYPE_TEXT: &str = "text";
116pub const RESPONSE_TYPE_CONFIRM: &str = "confirm";
117
118pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
122
123pub const SSH_PROMPT_TIMEOUT: Duration = Duration::from_secs(60);
125
126pub struct UserInputHandler {
131 channels: Mutex<HashMap<String, oneshot::Sender<UserInputResponse>>>,
132}
133
134impl UserInputHandler {
135 pub fn new() -> Self {
136 Self {
137 channels: Mutex::new(HashMap::new()),
138 }
139 }
140
141 pub fn register(&self, request_id: &str) -> oneshot::Receiver<UserInputResponse> {
144 let (tx, rx) = oneshot::channel();
145 let mut channels = self.channels.lock().unwrap();
146 channels.insert(request_id.to_string(), tx);
147 rx
148 }
149
150 pub fn deliver(&self, response: UserInputResponse) -> Result<(), String> {
153 let mut channels = self.channels.lock().unwrap();
154 let tx = channels
155 .remove(&response.request_id)
156 .ok_or_else(|| format!("no pending request: {}", response.request_id))?;
157 tx.send(response)
158 .map_err(|_| "receiver dropped".to_string())
159 }
160
161 pub fn cancel(&self, request_id: &str) {
163 let mut channels = self.channels.lock().unwrap();
164 channels.remove(request_id);
165 }
166
167 pub fn has_pending(&self, request_id: &str) -> bool {
169 let channels = self.channels.lock().unwrap();
170 channels.contains_key(request_id)
171 }
172
173 pub fn pending_count(&self) -> usize {
175 let channels = self.channels.lock().unwrap();
176 channels.len()
177 }
178}
179
180impl Default for UserInputHandler {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186pub async fn wait_for_response(
188 rx: oneshot::Receiver<UserInputResponse>,
189 timeout: Duration,
190) -> Result<UserInputResponse, String> {
191 match time::timeout(timeout, rx).await {
192 Ok(Ok(response)) => {
193 if response.is_error() {
194 Err(response.error_msg)
195 } else {
196 Ok(response)
197 }
198 }
199 Ok(Err(_)) => Err("user input handler was cancelled".to_string()),
200 Err(_) => Err("timed out waiting for user input".to_string()),
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_user_input_request_serde() {
210 let req = UserInputRequest {
211 request_id: "req-1".to_string(),
212 query_text: "Enter password:".to_string(),
213 response_type: RESPONSE_TYPE_TEXT.to_string(),
214 title: "SSH Authentication".to_string(),
215 markdown: true,
216 timeout_ms: 60000,
217 checkbox_msg: String::new(),
218 public_text: false,
219 ok_label: String::new(),
220 cancel_label: String::new(),
221 };
222 let json = serde_json::to_string(&req).unwrap();
223 assert!(json.contains("\"requestid\":\"req-1\""));
224 assert!(json.contains("\"querytext\":\"Enter password:\""));
225 assert!(json.contains("\"responsetype\":\"text\""));
226 assert!(json.contains("\"timeoutms\":60000"));
227 assert!(!json.contains("\"checkboxmsg\""));
229 assert!(!json.contains("\"oklabel\""));
230
231 let parsed: UserInputRequest = serde_json::from_str(&json).unwrap();
232 assert_eq!(parsed.request_id, "req-1");
233 assert!(parsed.markdown);
234 }
235
236 #[test]
237 fn test_user_input_response_serde() {
238 let resp = UserInputResponse {
239 response_type: RESPONSE_TYPE_TEXT.to_string(),
240 request_id: "req-1".to_string(),
241 text: "my_password".to_string(),
242 confirm: false,
243 error_msg: String::new(),
244 checkbox_stat: false,
245 };
246 let json = serde_json::to_string(&resp).unwrap();
247 assert!(json.contains("\"requestid\":\"req-1\""));
248 assert!(json.contains("\"text\":\"my_password\""));
249 assert!(!json.contains("\"errormsg\"")); let parsed: UserInputResponse = serde_json::from_str(&json).unwrap();
252 assert_eq!(parsed.text, "my_password");
253 }
254
255 #[test]
256 fn test_response_is_error() {
257 let resp = UserInputResponse {
258 error_msg: "cancelled".to_string(),
259 ..Default::default()
260 };
261 assert!(resp.is_error());
262 assert!(!resp.is_confirmed());
263 }
264
265 #[test]
266 fn test_response_is_confirmed() {
267 let resp = UserInputResponse {
268 confirm: true,
269 ..Default::default()
270 };
271 assert!(resp.is_confirmed());
272 assert!(!resp.is_error());
273 }
274
275 #[test]
276 fn test_handler_register_deliver() {
277 let handler = UserInputHandler::new();
278 let mut rx = handler.register("req-1");
279 assert!(handler.has_pending("req-1"));
280 assert_eq!(handler.pending_count(), 1);
281
282 let response = UserInputResponse {
283 request_id: "req-1".to_string(),
284 text: "hello".to_string(),
285 ..Default::default()
286 };
287 handler.deliver(response).unwrap();
288 assert!(!handler.has_pending("req-1"));
289
290 let received = rx.try_recv().unwrap();
291 assert_eq!(received.text, "hello");
292 }
293
294 #[test]
295 fn test_handler_deliver_no_pending() {
296 let handler = UserInputHandler::new();
297 let response = UserInputResponse {
298 request_id: "nonexistent".to_string(),
299 ..Default::default()
300 };
301 assert!(handler.deliver(response).is_err());
302 }
303
304 #[test]
305 fn test_handler_cancel() {
306 let handler = UserInputHandler::new();
307 let _rx = handler.register("req-1");
308 assert!(handler.has_pending("req-1"));
309 handler.cancel("req-1");
310 assert!(!handler.has_pending("req-1"));
311 }
312
313 #[tokio::test]
314 async fn test_wait_for_response_success() {
315 let handler = UserInputHandler::new();
316 let rx = handler.register("req-1");
317
318 tokio::spawn(async move {
320 tokio::time::sleep(Duration::from_millis(10)).await;
321 handler
322 .deliver(UserInputResponse {
323 request_id: "req-1".to_string(),
324 text: "password123".to_string(),
325 ..Default::default()
326 })
327 .unwrap();
328 });
329
330 let result = wait_for_response(rx, Duration::from_secs(5)).await;
331 assert!(result.is_ok());
332 assert_eq!(result.unwrap().text, "password123");
333 }
334
335 #[tokio::test]
336 async fn test_wait_for_response_error() {
337 let handler = UserInputHandler::new();
338 let rx = handler.register("req-1");
339
340 tokio::spawn(async move {
341 handler
342 .deliver(UserInputResponse {
343 request_id: "req-1".to_string(),
344 error_msg: "user cancelled".to_string(),
345 ..Default::default()
346 })
347 .unwrap();
348 });
349
350 let result = wait_for_response(rx, Duration::from_secs(5)).await;
351 assert!(result.is_err());
352 assert!(result.unwrap_err().contains("user cancelled"));
353 }
354
355 #[tokio::test]
356 async fn test_wait_for_response_timeout() {
357 let (_tx, rx) = oneshot::channel::<UserInputResponse>();
358 let result = wait_for_response(rx, Duration::from_millis(10)).await;
359 assert!(result.is_err());
360 assert!(result.unwrap_err().contains("timed out"));
361 }
362
363 #[test]
364 fn test_confirm_request_serde() {
365 let req = UserInputRequest {
366 request_id: "req-2".to_string(),
367 query_text: "Add host key?".to_string(),
368 response_type: RESPONSE_TYPE_CONFIRM.to_string(),
369 title: "SSH Host Key".to_string(),
370 markdown: true,
371 timeout_ms: 30000,
372 checkbox_msg: "Don't ask again".to_string(),
373 public_text: true,
374 ok_label: "Accept".to_string(),
375 cancel_label: "Reject".to_string(),
376 };
377 let json = serde_json::to_string(&req).unwrap();
378 assert!(json.contains("\"checkboxmsg\":\"Don't ask again\""));
379 assert!(json.contains("\"oklabel\":\"Accept\""));
380 assert!(json.contains("\"cancellabel\":\"Reject\""));
381 }
382}