1use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
2use windows_sys::Win32::Storage::FileSystem::{
3 CreateFileW, ReadFile, WriteFile, FILE_ATTRIBUTE_NORMAL,
4 OPEN_EXISTING,
5};
6use windows_sys::Win32::System::Pipes::WaitNamedPipeW;
7use windows_sys::Win32::System::Threading::{OpenProcess, QueryFullProcessImageNameW};
8use windows_sys::Win32::System::Diagnostics::ToolHelp::{
9 CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, PROCESSENTRY32W,
10};
11use sha2::{Sha256, Digest};
12
13use crate::error::{Mt5Error, Result};
14
15const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
16
17pub struct NamedPipeClient {
18 handle: HANDLE,
19}
20
21impl NamedPipeClient {
22 pub fn new(pipe_name: Option<&str>) -> Result<Self> {
23 let name = match pipe_name {
24 Some(n) => n.to_string(),
25 None => {
26 return Err(Mt5Error::ConnectionFailed(
27 "Pipe name must be provided. Use initialize(Some(\"pipe_name\"))".into(),
28 ))
29 }
30 };
31
32 let pipe_name_wide: Vec<u16> = name
33 .encode_utf16()
34 .chain(std::iter::once(0))
35 .collect();
36
37 unsafe {
38 WaitNamedPipeW(pipe_name_wide.as_ptr(), 500);
39 }
40
41 let handle = unsafe {
42 CreateFileW(
43 pipe_name_wide.as_ptr(),
44 0x80000000 | 0x40000000,
45 0,
46 std::ptr::null(),
47 OPEN_EXISTING,
48 FILE_ATTRIBUTE_NORMAL,
49 std::ptr::null_mut(),
50 )
51 };
52
53 if handle == INVALID_HANDLE_VALUE {
54 return Err(Mt5Error::ConnectionFailed(format!(
55 "Failed to connect to pipe: {}",
56 name
57 )));
58 }
59
60 Ok(Self { handle })
61 }
62
63 pub fn send(&self, cmd: u32, data: &[u8]) -> Result<Vec<u8>> {
64 let total_len = 4 + data.len();
65 let mut request = Vec::with_capacity(8 + data.len());
66 request.extend_from_slice(&(total_len as u32).to_le_bytes());
67 request.extend_from_slice(&cmd.to_le_bytes());
68 request.extend_from_slice(data);
69
70 unsafe {
71 let mut bytes_written = 0u32;
72 let result = WriteFile(
73 self.handle,
74 request.as_ptr(),
75 request.len() as u32,
76 &mut bytes_written,
77 std::ptr::null_mut(),
78 );
79
80 if result == 0 {
81 return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
82 }
83 }
84
85 self.read_response()
86 }
87
88 fn read_response(&self) -> Result<Vec<u8>> {
89 let mut len_buf = [0u8; 4];
90 let mut bytes_read = 0u32;
91
92 unsafe {
93 let result = ReadFile(
94 self.handle,
95 len_buf.as_mut_ptr(),
96 len_buf.len() as u32,
97 &mut bytes_read,
98 std::ptr::null_mut(),
99 );
100
101 if result == 0 {
102 return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
103 }
104 }
105
106 let payload_len = u32::from_le_bytes(len_buf) as usize;
107
108 if payload_len < 8 {
109 return Err(Mt5Error::InvalidResponse(format!(
110 "Payload too small: {} bytes",
111 payload_len
112 )));
113 }
114
115 let mut payload = vec![0u8; payload_len];
116 let mut total_read = 0usize;
117
118 while total_read < payload_len {
119 let mut bytes_read = 0u32;
120 unsafe {
121 let result = ReadFile(
122 self.handle,
123 payload[total_read..].as_mut_ptr(),
124 (payload_len - total_read) as u32,
125 &mut bytes_read,
126 std::ptr::null_mut(),
127 );
128
129 if result == 0 {
130 return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
131 }
132
133 total_read += bytes_read as usize;
134 }
135 }
136
137 let _cmd_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
138 let _success = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]) != 0;
139
140 if payload.len() > 8 {
141 Ok(payload[8..].to_vec())
142 } else {
143 Ok(Vec::new())
144 }
145 }
146}
147
148impl Drop for NamedPipeClient {
149 fn drop(&mut self) {
150 if self.handle != INVALID_HANDLE_VALUE {
151 unsafe {
152 CloseHandle(self.handle);
153 }
154 }
155 }
156}
157
158unsafe impl Send for NamedPipeClient {}
159
160pub fn compute_pipe_name(terminal_path: &str) -> String {
161 let input = format!(r"\\?\{}", terminal_path.to_lowercase());
162 let input_utf16: Vec<u16> = input.encode_utf16().collect();
163
164 let mut buf = Vec::with_capacity(input_utf16.len() * 2);
165 for c in input_utf16 {
166 buf.push(c as u8);
167 buf.push((c >> 8) as u8);
168 }
169
170 let mut hasher = Sha256::new();
171 hasher.update(&buf);
172 let result = hasher.finalize();
173
174 format!(r"\\.\pipe\MT5.Terminal.{}", hex::encode(result).to_uppercase())
175}
176
177pub fn discover_mt5_pipe() -> String {
178 let paths = find_terminal64_paths().unwrap_or_default();
179
180 for path in &paths {
181 let pipe_name = compute_pipe_name(path);
182 if test_pipe_connection(&pipe_name) {
183 return pipe_name;
184 }
185 }
186
187 panic!("No responding MT5 pipe found");
188}
189
190fn test_pipe_connection(pipe_name: &str) -> bool {
191 let pipe_name_wide: Vec<u16> = pipe_name
192 .encode_utf16()
193 .chain(std::iter::once(0))
194 .collect();
195
196 unsafe {
197 WaitNamedPipeW(pipe_name_wide.as_ptr(), 500);
198 }
199
200 let handle = unsafe {
201 CreateFileW(
202 pipe_name_wide.as_ptr(),
203 0x80000000 | 0x40000000,
204 0,
205 std::ptr::null(),
206 OPEN_EXISTING,
207 FILE_ATTRIBUTE_NORMAL,
208 std::ptr::null_mut(),
209 )
210 };
211
212 if handle != INVALID_HANDLE_VALUE {
213 unsafe {
214 CloseHandle(handle);
215 }
216 true
217 } else {
218 false
219 }
220}
221
222fn find_terminal64_paths() -> Result<Vec<String>> {
223 let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
224 if snapshot == INVALID_HANDLE_VALUE {
225 return Err(Mt5Error::ConnectionFailed(
226 "Failed to create process snapshot".into(),
227 ));
228 }
229
230 let mut paths = Vec::new();
231 let mut seen = std::collections::HashSet::new();
232
233 let mut pe = PROCESSENTRY32W {
234 dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
235 ..unsafe { std::mem::zeroed() }
236 };
237
238 let mut result = unsafe { Process32FirstW(snapshot, &mut pe) };
239 while result != 0 {
240 let exe_name = String::from_utf16_lossy(&pe.szExeFile)
241 .trim_end_matches('\0')
242 .to_lowercase();
243
244 if exe_name == "terminal64.exe" {
245 if let Ok(path) = get_process_path(pe.th32ProcessID) {
246 if seen.insert(path.clone()) {
247 paths.push(path);
248 }
249 }
250 }
251
252 result = unsafe { Process32NextW(snapshot, &mut pe) };
253 }
254
255 unsafe { CloseHandle(snapshot) };
256
257 if paths.is_empty() {
258 return Err(Mt5Error::ConnectionFailed(
259 "No running terminal64.exe found".into(),
260 ));
261 }
262
263 Ok(paths)
264}
265
266fn get_process_path(pid: u32) -> Result<String> {
267 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
268 if handle == std::ptr::null_mut() {
269 return Err(Mt5Error::ConnectionFailed(format!(
270 "Failed to open process {}",
271 pid
272 )));
273 }
274
275 let mut buf = [0u16; 32768];
276 let mut size = buf.len() as u32;
277
278 let result = unsafe { QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut size) };
279
280 unsafe { CloseHandle(handle) };
281
282 if result == 0 {
283 return Err(Mt5Error::ConnectionFailed(format!(
284 "Failed to get process image name for PID {}",
285 pid
286 )));
287 }
288
289 Ok(String::from_utf16_lossy(&buf[..size as usize]))
290}