mt5-rs 0.1.1

A pure Rust library for MetaTrader 5 IPC communication (no Python dependency)
Documentation
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{
    CreateFileW, ReadFile, WriteFile, FILE_ATTRIBUTE_NORMAL,
    OPEN_EXISTING,
};
use windows_sys::Win32::System::Pipes::WaitNamedPipeW;
use windows_sys::Win32::System::Threading::{OpenProcess, QueryFullProcessImageNameW};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
    CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, PROCESSENTRY32W,
};
use sha2::{Sha256, Digest};

use crate::error::{Mt5Error, Result};

const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;

pub struct NamedPipeClient {
    handle: HANDLE,
}

impl NamedPipeClient {
    pub fn new(pipe_name: Option<&str>) -> Result<Self> {
        let name = match pipe_name {
            Some(n) => n.to_string(),
            None => {
                return Err(Mt5Error::ConnectionFailed(
                    "Pipe name must be provided. Use initialize(Some(\"pipe_name\"))".into(),
                ))
            }
        };

        let pipe_name_wide: Vec<u16> = name
            .encode_utf16()
            .chain(std::iter::once(0))
            .collect();

        unsafe {
            WaitNamedPipeW(pipe_name_wide.as_ptr(), 500);
        }

        let handle = unsafe {
            CreateFileW(
                pipe_name_wide.as_ptr(),
                0x80000000 | 0x40000000,
                0,
                std::ptr::null(),
                OPEN_EXISTING,
                FILE_ATTRIBUTE_NORMAL,
                std::ptr::null_mut(),
            )
        };

        if handle == INVALID_HANDLE_VALUE {
            return Err(Mt5Error::ConnectionFailed(format!(
                "Failed to connect to pipe: {}",
                name
            )));
        }

        Ok(Self { handle })
    }

    pub fn send(&self, cmd: u32, data: &[u8]) -> Result<Vec<u8>> {
        let total_len = 4 + data.len();
        let mut request = Vec::with_capacity(8 + data.len());
        request.extend_from_slice(&(total_len as u32).to_le_bytes());
        request.extend_from_slice(&cmd.to_le_bytes());
        request.extend_from_slice(data);

        unsafe {
            let mut bytes_written = 0u32;
            let result = WriteFile(
                self.handle,
                request.as_ptr(),
                request.len() as u32,
                &mut bytes_written,
                std::ptr::null_mut(),
            );

            if result == 0 {
                return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
            }
        }

        self.read_response()
    }

    fn read_response(&self) -> Result<Vec<u8>> {
        let mut len_buf = [0u8; 4];
        let mut bytes_read = 0u32;

        unsafe {
            let result = ReadFile(
                self.handle,
                len_buf.as_mut_ptr(),
                len_buf.len() as u32,
                &mut bytes_read,
                std::ptr::null_mut(),
            );

            if result == 0 {
                return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
            }
        }

        let payload_len = u32::from_le_bytes(len_buf) as usize;

        if payload_len < 8 {
            return Err(Mt5Error::InvalidResponse(format!(
                "Payload too small: {} bytes",
                payload_len
            )));
        }

        let mut payload = vec![0u8; payload_len];
        let mut total_read = 0usize;

        while total_read < payload_len {
            let mut bytes_read = 0u32;
            unsafe {
                let result = ReadFile(
                    self.handle,
                    payload[total_read..].as_mut_ptr(),
                    (payload_len - total_read) as u32,
                    &mut bytes_read,
                    std::ptr::null_mut(),
                );

                if result == 0 {
                    return Err(Mt5Error::IoError(std::io::Error::last_os_error()));
                }

                total_read += bytes_read as usize;
            }
        }

        let _cmd_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
        let _success = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]) != 0;

        if payload.len() > 8 {
            Ok(payload[8..].to_vec())
        } else {
            Ok(Vec::new())
        }
    }
}

impl Drop for NamedPipeClient {
    fn drop(&mut self) {
        if self.handle != INVALID_HANDLE_VALUE {
            unsafe {
                CloseHandle(self.handle);
            }
        }
    }
}

unsafe impl Send for NamedPipeClient {}

pub fn compute_pipe_name(terminal_path: &str) -> String {
    let input = format!(r"\\?\{}", terminal_path.to_lowercase());
    let input_utf16: Vec<u16> = input.encode_utf16().collect();

    let mut buf = Vec::with_capacity(input_utf16.len() * 2);
    for c in input_utf16 {
        buf.push(c as u8);
        buf.push((c >> 8) as u8);
    }

    let mut hasher = Sha256::new();
    hasher.update(&buf);
    let result = hasher.finalize();

    format!(r"\\.\pipe\MT5.Terminal.{}", hex::encode(result).to_uppercase())
}

pub fn discover_mt5_pipe() -> String {
    let paths = find_terminal64_paths().unwrap_or_default();

    for path in &paths {
        let pipe_name = compute_pipe_name(path);
        if test_pipe_connection(&pipe_name) {
            return pipe_name;
        }
    }

    panic!("No responding MT5 pipe found");
}

fn test_pipe_connection(pipe_name: &str) -> bool {
    let pipe_name_wide: Vec<u16> = pipe_name
        .encode_utf16()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        WaitNamedPipeW(pipe_name_wide.as_ptr(), 500);
    }

    let handle = unsafe {
        CreateFileW(
            pipe_name_wide.as_ptr(),
            0x80000000 | 0x40000000,
            0,
            std::ptr::null(),
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            std::ptr::null_mut(),
        )
    };

    if handle != INVALID_HANDLE_VALUE {
        unsafe {
            CloseHandle(handle);
        }
        true
    } else {
        false
    }
}

fn find_terminal64_paths() -> Result<Vec<String>> {
    let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
    if snapshot == INVALID_HANDLE_VALUE {
        return Err(Mt5Error::ConnectionFailed(
            "Failed to create process snapshot".into(),
        ));
    }

    let mut paths = Vec::new();
    let mut seen = std::collections::HashSet::new();

    let mut pe = PROCESSENTRY32W {
        dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
        ..unsafe { std::mem::zeroed() }
    };

    let mut result = unsafe { Process32FirstW(snapshot, &mut pe) };
    while result != 0 {
        let exe_name = String::from_utf16_lossy(&pe.szExeFile)
            .trim_end_matches('\0')
            .to_lowercase();

        if exe_name == "terminal64.exe" {
            if let Ok(path) = get_process_path(pe.th32ProcessID) {
                if seen.insert(path.clone()) {
                    paths.push(path);
                }
            }
        }

        result = unsafe { Process32NextW(snapshot, &mut pe) };
    }

    unsafe { CloseHandle(snapshot) };

    if paths.is_empty() {
        return Err(Mt5Error::ConnectionFailed(
            "No running terminal64.exe found".into(),
        ));
    }

    Ok(paths)
}

fn get_process_path(pid: u32) -> Result<String> {
    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
    if handle == std::ptr::null_mut() {
        return Err(Mt5Error::ConnectionFailed(format!(
            "Failed to open process {}",
            pid
        )));
    }

    let mut buf = [0u16; 32768];
    let mut size = buf.len() as u32;

    let result = unsafe { QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut size) };

    unsafe { CloseHandle(handle) };

    if result == 0 {
        return Err(Mt5Error::ConnectionFailed(format!(
            "Failed to get process image name for PID {}",
            pid
        )));
    }

    Ok(String::from_utf16_lossy(&buf[..size as usize]))
}