Skip to main content

mt5_rs/
protocol.rs

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}