fission_shell_desktop/
test_control.rs1use fission_test_driver::{TestCommand, TestResponse, TextItem, SemanticNode};
2use std::io::{Read, Write};
3use std::net::{TcpListener, TcpStream};
4use std::sync::mpsc;
5
6pub type CommandSender = mpsc::Sender<(TestCommand, ResponseSender)>;
7pub type CommandReceiver = mpsc::Receiver<(TestCommand, ResponseSender)>;
8pub type ResponseSender = mpsc::Sender<TestResponse>;
9
10pub fn create_channel() -> (CommandSender, CommandReceiver) {
11 mpsc::channel()
12}
13
14pub fn spawn_server(port: u16, cmd_tx: CommandSender) -> std::thread::JoinHandle<()> {
15 std::thread::spawn(move || {
16 let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
17 .unwrap_or_else(|e| panic!("failed to bind test control port {}: {}", port, e));
18 eprintln!("[fission-test-control] listening on port {}", port);
19
20 for stream in listener.incoming() {
21 match stream {
22 Ok(stream) => handle_connection(stream, &cmd_tx),
23 Err(e) => eprintln!("[fission-test-control] accept error: {}", e),
24 }
25 }
26 })
27}
28
29fn handle_connection(mut stream: TcpStream, cmd_tx: &CommandSender) {
30 let mut buf = Vec::new();
31 let mut tmp = [0u8; 4096];
32
33 loop {
35 match stream.read(&mut tmp) {
36 Ok(0) => return,
37 Ok(n) => {
38 buf.extend_from_slice(&tmp[..n]);
39 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
41 break;
42 }
43 }
44 Err(_) => return,
45 }
46 }
47
48 let request = String::from_utf8_lossy(&buf);
49
50 let first_line = request.lines().next().unwrap_or("");
52 let parts: Vec<&str> = first_line.split_whitespace().collect();
53 let method = parts.first().copied().unwrap_or("");
54 let path = parts.get(1).copied().unwrap_or("");
55
56 if path == "/health" {
58 send_http_response(&mut stream, 200, r#"{"status":"ok"}"#);
59 return;
60 }
61
62 if method != "POST" || path != "/cmd" {
63 send_http_response(&mut stream, 404, r#"{"status":"Error","message":"not found"}"#);
64 return;
65 }
66
67 let content_length = request
69 .lines()
70 .find(|l| l.to_lowercase().starts_with("content-length:"))
71 .and_then(|l| l.split(':').nth(1))
72 .and_then(|v| v.trim().parse::<usize>().ok())
73 .unwrap_or(0);
74
75 let header_end = buf
77 .windows(4)
78 .position(|w| w == b"\r\n\r\n")
79 .map(|p| p + 4)
80 .unwrap_or(buf.len());
81
82 let mut body = buf[header_end..].to_vec();
83
84 while body.len() < content_length {
86 match stream.read(&mut tmp) {
87 Ok(0) => break,
88 Ok(n) => body.extend_from_slice(&tmp[..n]),
89 Err(_) => break,
90 }
91 }
92
93 let body_str = String::from_utf8_lossy(&body);
94
95 let cmd: TestCommand = match serde_json::from_str(&body_str) {
97 Ok(c) => c,
98 Err(e) => {
99 let resp = TestResponse::Error {
100 message: format!("parse error: {}", e),
101 };
102 send_http_response(&mut stream, 400, &serde_json::to_string(&resp).unwrap());
103 return;
104 }
105 };
106
107 let (resp_tx, resp_rx) = mpsc::channel();
109 if cmd_tx.send((cmd, resp_tx)).is_err() {
110 send_http_response(
111 &mut stream,
112 500,
113 r#"{"status":"Error","message":"event loop disconnected"}"#,
114 );
115 return;
116 }
117
118 match resp_rx.recv_timeout(std::time::Duration::from_secs(30)) {
120 Ok(response) => {
121 send_http_response(&mut stream, 200, &serde_json::to_string(&response).unwrap());
122 }
123 Err(_) => {
124 send_http_response(
125 &mut stream,
126 504,
127 r#"{"status":"Error","message":"timeout waiting for response"}"#,
128 );
129 }
130 }
131}
132
133fn send_http_response(stream: &mut TcpStream, status: u16, body: &str) {
134 let status_text = match status {
135 200 => "OK",
136 400 => "Bad Request",
137 404 => "Not Found",
138 500 => "Internal Server Error",
139 504 => "Gateway Timeout",
140 _ => "Unknown",
141 };
142 let response = format!(
143 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
144 status, status_text, body.len(), body
145 );
146 let _ = stream.write_all(response.as_bytes());
147}