commit d247e838b40b81b9ddc663029dfca7b9a1dff747 Author: beerpsi Date: Sun Dec 24 03:39:58 2023 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c98f1e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ +/target +/Cargo.lock + +# Created by https://www.toptal.com/developers/gitignore/api/jetbrains +# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/jetbrains + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..1c2fda5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/chuniio-yubideck.iml b/.idea/chuniio-yubideck.iml new file mode 100644 index 0000000..bf4ce7b --- /dev/null +++ b/.idea/chuniio-yubideck.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2d9efaf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..63752cb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "chuniio-yubideck" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[profile.release] +strip = true # Automatically strip symbols from the binary. +opt-level = "z" # Optimize for size. +lto = true +codegen-units = 1 +panic = "abort" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +chusan = [] + +[dependencies] +anyhow = "1.0.76" +env_logger = { version = "0.10.1", default-features = false } +lazy_static = "1.4.0" +log = "0.4.20" +rusb = "0.9.3" +shared_memory = { path = "vendor/shared_memory" } +winapi = { version = "0.3.9", features = ["minwindef", "winnt", "winerror", "debugapi", "winbase", "winuser"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc51daa --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# chuniio-yubideck +ChuniIO driver for YubiDeck FW 3.0. + +**UNTESTED!!!** Here be dragons. + +Thanks to: +- [hlcm0](https://github.com/hlcm0/yubideck-io-firmware/blob/main/code/pico_yubideck_emu/report.h) and [4yn](https://github.com/4yn/slidershim/blob/main/src-slider_io/src/device/hid.rs#L257) +for YubiDeck protocol information + +## Configuration +segatools.ini + +```ini +[chuniio] +;; For Chunithm NEW or newer +path32=chuniio_yubideck_chusan.dll +path64=chuniio_yubideck_amdaemon.dll + +;; For CHUNITHM PARADISE and older +path=chuniio_yubideck.dll + +[io3] +test=0x31 +service=0x32 +coin=0x33 +``` + +## USB Protocol +USB device: `1973:2001`, interface 0 + +- Endpoint IN Interrupt (0x81) + - Data length: 45 bytes + - `data[0]`: bits 0-5: beam 1-6 (1 = blocked) + - `data[1]`: bits 0-2 for the 3 buttons (1 = pressed) + - `data[2..34]`: pressure of touch sensor 1-32 (counting from top left) + - `data[34]`: Card type + - 0: No card + - 1: MIFARE Classic + - 2: FeliCa + - `data[35..45]`: Card IDm/Access Code + +- Endpoint OUT Interrupt (0x02) + - Data length: 61 bytes + - `data[0]`: Packet type + - Packet type 1: + - `data[1..61]`: Slider LED for the first 20 sensors, RGB + - Packet type 2: + - `data[1..34]`: Slider LED for the last 11 sensors, RGB + - `data[34..37]`: Left air LED, RGB + - `data[37..40]`: Right air LED, RGB + - `data[40..43]`: Card reader LED, RGB + - `data[43..61]`: Empty padding + +## Build instructions +```shell +# For CHUNITHM NEW +cargo build --features chusan --target i686-pc-windows-msvc --release +cargo build --features chusan --target x86_64-pc-windows-msvc --release +cp target/i686-pc-windows-msvc/release/chuniio_yubideck.dll chuniio_yubideck_chusan.dll +cp target/x86_64-pc-windows-msvc/release/chuniio_yubideck.dll chuniio_yubideck_amdaemon.dll + +# For CHUNITHM +cargo build --target i686-pc-windows-msvc --release +cp target/i686-pc-windows-msvc/release/chuniio_yubideck.dll . +``` \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0c3b472 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +imports_granularity = "Crate" +reorder_imports = true +group_imports = "StdExternalCrate" diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..8732dbf --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, Copy)] +pub struct Configuration { + pub test_key: u32, + pub service_key: u32, + pub coin_key: u32, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..76ba9b1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,467 @@ +mod configuration; +mod log; + +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +use std::{ffi::c_int, sync::atomic::AtomicU16}; +use std::{ + ffi::{c_void, CString}, + fmt::Display, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, OnceLock, + }, + thread, + time::Duration, +}; +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +use std::{sync::Mutex, thread::JoinHandle}; + +use ::log::{error, info}; +use anyhow::{anyhow, Result}; +use lazy_static::lazy_static; +use rusb::{DeviceHandle, GlobalContext}; +use shared_memory::{Shmem, ShmemConf, ShmemError}; +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +use winapi::um::winuser::GetAsyncKeyState; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, TRUE}, + winerror::{E_FAIL, S_OK}, + }, + um::{ + winbase::GetPrivateProfileIntA, + winnt::{DLL_PROCESS_ATTACH, HRESULT}, + }, +}; + +use crate::{configuration::Configuration, log::init_logger}; + +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +type SliderCallbackFn = unsafe extern "C" fn(data: *const u8); + +static DEVICE: OnceLock> = OnceLock::new(); +static mut INPUT_SHMEM: Option> = None; + +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +static mut SLIDER_THREAD: OnceLock> = OnceLock::new(); + +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +static SLIDER_ACTIVE: AtomicBool = AtomicBool::new(false); + +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +static COIN_COUNT: AtomicU16 = AtomicU16::new(0); + +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +static COIN_PRESSED: AtomicBool = AtomicBool::new(false); + +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +static mut OUTPUT_SHMEM: Option>> = None; + +lazy_static! { + static ref CONFIGURATION: Configuration = { + let io3 = CString::new("io3").unwrap(); + let test = CString::new("test").unwrap(); + let service = CString::new("service").unwrap(); + let coin = CString::new("coin").unwrap(); + let cfg_file = CString::new(".\\segatools.ini").unwrap(); + + unsafe { + Configuration { + test_key: GetPrivateProfileIntA( + io3.as_ptr(), + test.as_ptr(), + 0x31, + cfg_file.as_ptr(), + ), + service_key: GetPrivateProfileIntA( + io3.as_ptr(), + service.as_ptr(), + 0x32, + cfg_file.as_ptr(), + ), + coin_key: GetPrivateProfileIntA( + io3.as_ptr(), + coin.as_ptr(), + 0x33, + cfg_file.as_ptr(), + ), + } + } + }; +} + +#[no_mangle] +extern "system" fn DllMain(_dll_module: HINSTANCE, call_reason: DWORD, _reserved: LPVOID) -> BOOL { + match call_reason { + DLL_PROCESS_ATTACH => init_logger(), + _ => {} + } + + TRUE +} + +#[no_mangle] +pub extern "C" fn chuni_io_get_api_version() -> u16 { + 0x0101 +} + +#[no_mangle] +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +pub extern "C" fn chuni_io_jvs_init() -> HRESULT { + if cfg!(not(feature = "chusan")) { + if let Err(e) = yubideck_init() { + error!("Could not initialize YubiDeck: {e:#?}"); + return E_FAIL; + } + } + + return create_input_shared_memory(); +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86"))] +pub extern "C" fn chuni_io_jvs_init() -> HRESULT { + S_OK +} + +#[no_mangle] +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +pub extern "C" fn chuni_io_jvs_poll(opbtn: *mut u8, beams: *mut u8) { + if opbtn.is_null() || beams.is_null() { + return; + } + + let Some(input_shmem) = (unsafe { &INPUT_SHMEM }) else { + return; + }; + let input = unsafe { input_shmem.as_slice() }; + let ir_value = input[0]; + let mut buttons = input[1] & 3; // Buttons are in order: coin, service, test. We take the last 2 bits + + unsafe { + if GetAsyncKeyState(CONFIGURATION.test_key as c_int) != 0 { + buttons |= 1; + } + + if GetAsyncKeyState(CONFIGURATION.service_key as c_int) != 0 { + buttons |= 2; + } + + *opbtn = buttons; + *beams = ir_value; + } +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86"))] +pub extern "C" fn chuni_io_jvs_poll(_opbtn: *mut u8, _beams: *mut u8) {} + +#[no_mangle] +#[cfg(any( + all(feature = "chusan", target_arch = "x86_64"), + not(feature = "chusan") +))] +pub extern "C" fn chuni_io_jvs_read_coin_counter(total: *mut u16) { + if total.is_null() { + return; + } + + let Some(input_shmem) = (unsafe { &INPUT_SHMEM }) else { + return; + }; + let input = unsafe { input_shmem.as_slice() }; + let coin_pressed = (input[1] & 4) != 0; + + if coin_pressed || unsafe { GetAsyncKeyState(CONFIGURATION.coin_key as c_int) } != 0 { + let coin_previously_pressed = COIN_PRESSED.fetch_or(true, Ordering::Relaxed); + if !coin_previously_pressed { + COIN_COUNT.fetch_add(1, Ordering::Relaxed); + } + } else { + COIN_PRESSED.store(false, Ordering::Relaxed); + } + + unsafe { + *total = COIN_COUNT.load(Ordering::Relaxed); + } +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86"))] +pub extern "C" fn chuni_io_jvs_read_coin_counter(_total: *mut u16) {} + +#[no_mangle] +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +pub unsafe extern "C" fn chuni_io_slider_init() -> HRESULT { + match create_shared_memory("Local\\YubideckOutput", 122, true) { + Ok(s) => OUTPUT_SHMEM = Some(Arc::new(Mutex::new(s))), + Err(e) => { + error!("Could not obtain shared memory: {e:#?}"); + return E_FAIL; + } + } + + if cfg!(not(feature = "chusan")) { + // Already initialized in chuni_io_jvs_init() + return S_OK; + } + + if let Err(e) = yubideck_init() { + error!("Failed to initialize YubiDeck: {e:#?}"); + return E_FAIL; + } + + return create_input_shared_memory(); +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86_64"))] +pub extern "C" fn chuni_io_slider_init(_callback: *const c_void) -> HRESULT { + S_OK +} + +#[no_mangle] +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +pub unsafe extern "C" fn chuni_io_slider_start(callback: *const c_void) { + if callback.is_null() { + return; + } + + if SLIDER_THREAD.get().is_some() { + return; + } + + SLIDER_ACTIVE.store(true, Ordering::Relaxed); + + let callback = std::mem::transmute::<_, SliderCallbackFn>(callback); + let thread = thread::spawn(move || { + let Some(input_shmem) = (unsafe { &INPUT_SHMEM }) else { + return; + }; + let usb_in = input_shmem.as_slice(); + let mut pressure = [0u8; 32]; + + while SLIDER_ACTIVE.load(Ordering::Relaxed) { + pressure.copy_from_slice(&usb_in[2..34]); + + for i in 0..16 { + pressure.swap(i * 2, i * 2 + 1); + } + + callback(pressure.as_ptr()); + thread::sleep(Duration::from_nanos(1_000_000)); + } + }); + + SLIDER_THREAD.set(thread).unwrap(); +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86_64"))] +pub extern "C" fn chuni_io_slider_start(_callback: *const c_void) {} + +#[no_mangle] +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +pub extern "C" fn chuni_io_slider_stop() { + let Some(thread) = (unsafe { SLIDER_THREAD.take() }) else { + return; + }; + + SLIDER_ACTIVE.store(false, Ordering::Relaxed); + + thread.join().expect("Couldn't join slider input thread"); +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86_64"))] +pub extern "C" fn chuni_io_slider_stop() {} + +#[no_mangle] +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +pub unsafe extern "C" fn chuni_io_slider_set_leds(rgb: *const u8) { + if rgb.is_null() { + return; + } + + let Some(device) = DEVICE.get() else { return }; + let Some(out_shmem) = &OUTPUT_SHMEM else { + error!("OUTPUT_SHMEM is unset."); + return; + }; + let Ok(mut out_shmem) = out_shmem.lock() else { + error!("Could not acquire mutex of output shared memory"); + return; + }; + let buf = out_shmem.as_slice_mut(); + let ground = std::slice::from_raw_parts(rgb, 93); + + buf[0] = 0; + buf[61] = 1; + + for (buf_chunk, state_chunk) in buf[1..61] + .chunks_mut(3) + .zip(ground.chunks(3).skip(11).take(20).rev()) + { + buf_chunk[0] = state_chunk[0]; + buf_chunk[1] = state_chunk[1]; + buf_chunk[2] = state_chunk[2]; + } + + for (buf_chunk, state_chunk) in buf[62..95] + .chunks_mut(3) + .zip(ground.chunks(3).take(11).rev()) + { + buf_chunk[0] = state_chunk[0]; + buf_chunk[1] = state_chunk[1]; + buf_chunk[2] = state_chunk[2]; + } + + if let Err(e) = device.write_interrupt(0x02, &buf[0..61], Duration::from_millis(20)) { + error!("Error writing first batch of output data: {e:#?}"); + } + + if let Err(e) = device.write_interrupt(0x02, &buf[61..], Duration::from_millis(20)) { + error!("Error writing second batch of output data: {e:#?}"); + } +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86_64"))] +pub extern "C" fn chuni_io_slider_set_leds(_rgb: *const u8) {} + +#[no_mangle] +pub extern "C" fn chuni_io_led_init() -> HRESULT { + S_OK +} + +#[no_mangle] +#[cfg(any(all(feature = "chusan", target_arch = "x86"), not(feature = "chusan")))] +pub unsafe extern "C" fn chuni_io_led_set_colors(board: u8, rgb: *const u8) { + let Some(device) = DEVICE.get() else { return }; + let Some(out_shmem) = &OUTPUT_SHMEM else { + error!("OUTPUT_SHMEM is unset."); + return; + }; + let Ok(mut out_shmem) = out_shmem.lock() else { + error!("Could not acquire mutex of output shared memory"); + return; + }; + let buf = out_shmem.as_slice_mut(); + let data = std::slice::from_raw_parts(rgb, 183); + + buf[61] = 1; + + match board { + 0 => { + // left air + buf[95] = data[150]; + buf[96] = data[151]; + buf[97] = data[152]; + } + 1 => { + // right air + buf[98] = data[180]; + buf[99] = data[181]; + buf[100] = data[182]; + } + _ => {} + } + + if let Err(e) = device.write_interrupt(0x02, &buf[61..], Duration::from_millis(20)) { + error!("Error writing second batch of output data: {e:#?}"); + } +} + +#[no_mangle] +#[cfg(all(feature = "chusan", target_arch = "x86_64"))] +pub extern "C" fn chuni_io_led_set_colors(_rgb: *const u8) {} + +fn create_input_shared_memory() -> HRESULT { + match create_shared_memory("Local\\YubideckInput", 45, false) { + Ok(s) => { + unsafe { INPUT_SHMEM = Some(Arc::new(s)) }; + S_OK + } + Err(e) => { + error!("Could not acquire shared memory: {e:#?}"); + E_FAIL + } + } +} + +fn create_shared_memory(os_id: S, size: usize, is_owner: bool) -> Result +where + S: AsRef + Display + Copy, +{ + let shmem_conf = ShmemConf::new().size(size).os_id(os_id); + let shmem_result = match shmem_conf.clone().create() { + Ok(s) => Ok(s), + Err(ShmemError::MappingIdExists) => shmem_conf.open(), + Err(e) => { + return Err(anyhow!( + "Failed to create/open shared memory {os_id}: {e:#}" + )); + } + }; + + shmem_result + .map(|mut m| { + m.set_owner(is_owner); + m + }) + .map_err(|e| anyhow!("Failed to create/open shared memory {os_id}: {e:#}")) +} + +fn yubideck_init() -> Result<()> { + let Some(mut device) = rusb::open_device_with_vid_pid(0x1973, 0x2001) else { + return Err(anyhow!("YubiDeck not found.")); + }; + + device.set_active_configuration(1)?; + device.claim_interface(0)?; + DEVICE + .set(device) + .map_err(|e| anyhow!("Cannot store device handle: {e:#?}"))?; + + thread::spawn(input_thread_proc); + + Ok(()) +} + +fn input_thread_proc() { + info!("Input thread started"); + + let mut shmem = match create_shared_memory("Local\\YubideckInput", 45, true) { + Ok(s) => s, + Err(e) => { + error!("Could not obtain shared memory for YubiDeck input: {e:#}"); + return; + } + }; + let usb_in = unsafe { shmem.as_slice_mut() }; + let device = DEVICE.get().unwrap(); + + loop { + if let Err(e) = device.read_interrupt(0x81, usb_in, Duration::from_millis(20)) { + error!("Failed to read data from YubiDeck: {e:#}"); + } + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..134ae11 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,42 @@ +use std::{ffi::CString, io::Write}; + +use winapi::um::debugapi::OutputDebugStringA; + +#[derive(Debug)] +pub struct Logger {} + +impl Write for Logger { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Ok(c_str) = CString::new(buf) { + unsafe { OutputDebugStringA(c_str.as_ptr()) } + } + + std::io::stdout().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + std::io::stdout().flush() + } +} + +pub fn init_logger() { + env_logger::builder() + .filter_level(::log::LevelFilter::Error) + .filter_module( + "chuniio-tasoller", + if cfg!(debug_assertions) { + ::log::LevelFilter::Debug + } else { + ::log::LevelFilter::Info + }, + ) + .parse_default_env() + .target(env_logger::Target::Pipe(Box::new(Logger {}))) + .format(|f, record| { + let target = record.target(); + let level = record.level(); + + writeln!(f, "{} {} -> {}", level, target, record.args()) + }) + .init(); +} diff --git a/vendor/shared_memory/.github/dependabot.yml b/vendor/shared_memory/.github/dependabot.yml new file mode 100644 index 0000000..1116676 --- /dev/null +++ b/vendor/shared_memory/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/vendor/shared_memory/.github/workflows/rust.yml b/vendor/shared_memory/.github/workflows/rust.yml new file mode 100644 index 0000000..ee64334 --- /dev/null +++ b/vendor/shared_memory/.github/workflows/rust.yml @@ -0,0 +1,58 @@ +name: build + +on: + push: + branches: [master] + pull_request: + +jobs: + build: + name: Build + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + toolchain: [stable] + runs-on: ${{ matrix.platform }} + + steps: + - name: Checkout Sources + uses: actions/checkout@v2.4.0 + + - name: Install Rust Toolchain + uses: actions-rs/toolchain@v1.0.7 + with: + profile: minimal + toolchain: ${{ matrix.toolchain }} + override: true + components: rustfmt, clippy + + - name: Check Code Format + uses: actions-rs/cargo@v1.0.3 + with: + command: fmt + args: -- --check + + - name: Code Lint + uses: actions-rs/cargo@v1.0.3 + with: + command: clippy + args: --all-targets -- -D warnings + + - name: Build + uses: actions-rs/cargo@v1.0.3 + with: + command: build + + - name: Tests + uses: actions-rs/cargo@v1.0.3 + with: + command: test + + - name: Examples + uses: actions-rs/cargo@v1.0.3 + env: + RUST_LOG: "trace" + with: + command: run + args: --all-features --example mutex -- 15 diff --git a/vendor/shared_memory/.gitignore b/vendor/shared_memory/.gitignore new file mode 100644 index 0000000..e8f64bb --- /dev/null +++ b/vendor/shared_memory/.gitignore @@ -0,0 +1,5 @@ +/target +**/*.rs.bk +Cargo.lock +.vscode/ +/.idea diff --git a/vendor/shared_memory/Cargo.toml b/vendor/shared_memory/Cargo.toml new file mode 100644 index 0000000..4f61abe --- /dev/null +++ b/vendor/shared_memory/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "shared_memory" +description = "A user friendly crate that allows you to share memory between processes" +version = "0.12.4-chuniio" +authors = ["ElasT0ny "] +license = "MIT OR Apache-2.0" +edition = "2018" + +#Extra fields for crates.io +readme = "README.md" +documentation = "https://docs.rs/shared_memory" +repository = "https://github.com/elast0ny/shared_memory-rs" +keywords = ["shmem", "shared", "memory", "inter-process", "process"] +categories = [ + "os::unix-apis", + "os::windows-apis", + "memory-management", + "concurrency", + "asynchronous", +] + +exclude = ["ci/*", ".github/*"] + +[features] +default = [] +logging = ["log"] + +[dependencies] +cfg-if = "1.0" +log = { version = "0.4", optional = true } + +[target.'cfg(unix)'.dependencies] +nix = "0.23" +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +win-sys = "0.3" + +[dev-dependencies] +raw_sync = "0.1" +clap = {version = "3", features = ["derive"]} +env_logger = "0" diff --git a/vendor/shared_memory/README.md b/vendor/shared_memory/README.md new file mode 100644 index 0000000..cb2ce44 --- /dev/null +++ b/vendor/shared_memory/README.md @@ -0,0 +1,33 @@ +# shared_memory +[![Build Status](https://github.com/elast0ny/shared_memory-rs/workflows/build/badge.svg)](https://github.com/elast0ny/shared_memory-rs/actions?query=workflow%3Abuild) +[![crates.io](https://img.shields.io/crates/v/shared_memory.svg)](https://crates.io/crates/shared_memory) +[![mio](https://docs.rs/shared_memory/badge.svg)](https://docs.rs/shared_memory/) +[![Lines of Code](https://tokei.rs/b1/github/elast0ny/shared_memory-rs?category=code)](https://tokei.rs/b1/github/elast0ny/shared_memory-rs?category=code) + +A crate that allows you to share memory between __processes__. + +This crate provides lightweight wrappers around shared memory APIs in an OS agnostic way. It is intended to be used with it's sister crate [raw_sync](https://github.com/elast0ny/raw_sync-rs) which provide simple primitves to synchronize access to the shared memory (Mutex, RwLock, Events, etc...). + +| raw_sync | +|----| +|[![crates.io](https://img.shields.io/crates/v/raw_sync.svg)](https://crates.io/crates/raw_sync) [![docs.rs](https://docs.rs/raw_sync/badge.svg)](https://docs.rs/raw_sync/)| + +## Usage + +For usage examples, see code located in [examples/](examples/) : + + | Examples | Description | + |----------|-------------| + |[event](examples/event.rs)| Shows the use of shared events through shared memory| + |[mutex](examples/mutex.rs)| Shows the use of a shared mutex through shared memory| + +## License + + * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://opensource.org/licenses/MIT) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. diff --git a/vendor/shared_memory/changelog.md b/vendor/shared_memory/changelog.md new file mode 100644 index 0000000..ba19aa9 --- /dev/null +++ b/vendor/shared_memory/changelog.md @@ -0,0 +1,20 @@ +# Changelog + +# 0.12.4 +- Allow Windows users to open shared memory that isnt managed by this crate +- Added tests + +# 0.12.2 +- Default feature behavior is to disable logging on release builds +- Reverted edition bump back to 2018 +- Updated to use Microsoft's `windows-rs` crate + +# ~~0.12.1 (yanked)~~ +- ~~Updated to latest edition (2021)~~ + +# 0.12.0 +- Windows implementation now follows POSIX behavior in regards to ownership and deletion, see [#59](https://github.com/elast0ny/shared_memory-rs/pull/59) for more details +# __0.11.X__ +This release breaks backwards compatibility and removes a bunch of previous features which hid many unsafe behaviors (automatically casting shared memory to Rust types). + +The release also marks the split between `shared_memory` and its synchronization primitives into a seperate crate `raw_sync`. \ No newline at end of file diff --git a/vendor/shared_memory/examples/basic.rs b/vendor/shared_memory/examples/basic.rs new file mode 100644 index 0000000..2cf0f8a --- /dev/null +++ b/vendor/shared_memory/examples/basic.rs @@ -0,0 +1,78 @@ +use std::thread; + +use clap::Parser; +use shared_memory::*; + +/// Spawns N threads that increment a value to 100 +#[derive(Parser)] +#[clap(author, version, about)] +struct Args { + /// Number of threads to spawn + num_threads: usize, + + /// Count to this value + #[clap(long, short, default_value_t = 50)] + count_to: u8, +} + +fn main() { + env_logger::init(); + let args = Args::parse(); + + if args.num_threads < 1 { + eprintln!("Invalid number of threads"); + return; + } + + let mut threads = Vec::with_capacity(args.num_threads); + let _ = std::fs::remove_file("basic_mapping"); + let max = args.count_to; + // Spawn N threads + for i in 0..args.num_threads { + let thread_id = i + 1; + threads.push(thread::spawn(move || { + increment_value("basic_mapping", thread_id, max); + })); + } + + // Wait for threads to exit + for t in threads.drain(..) { + t.join().unwrap(); + } +} + +/// Increments a value that lives in shared memory +fn increment_value(shmem_flink: &str, thread_num: usize, max: u8) { + // Create or open the shared memory mapping + let shmem = match ShmemConf::new().size(4096).flink(shmem_flink).create() { + Ok(m) => m, + Err(ShmemError::LinkExists) => ShmemConf::new().flink(shmem_flink).open().unwrap(), + Err(e) => { + eprintln!( + "Unable to create or open shmem flink {} : {}", + shmem_flink, e + ); + return; + } + }; + + // Get pointer to the shared memory + let raw_ptr = shmem.as_ptr(); + + // WARNING: This is prone to race conditions as no sync/locking is used + unsafe { + while std::ptr::read_volatile(raw_ptr) < max { + // Increment shared value by one + *raw_ptr += 1; + + println!( + "[thread:{}] {}", + thread_num, + std::ptr::read_volatile(raw_ptr) + ); + + // Sleep for a bit + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } +} diff --git a/vendor/shared_memory/examples/event.rs b/vendor/shared_memory/examples/event.rs new file mode 100644 index 0000000..a08ce1d --- /dev/null +++ b/vendor/shared_memory/examples/event.rs @@ -0,0 +1,36 @@ +use raw_sync::{events::*, Timeout}; +use shared_memory::*; + +fn main() -> Result<(), Box> { + env_logger::init(); + // Attempt to create a mapping or open if it already exists + println!("Getting the shared memory mapping"); + let shmem = match ShmemConf::new().size(4096).flink("event_mapping").create() { + Ok(m) => m, + Err(ShmemError::LinkExists) => ShmemConf::new().flink("event_mapping").open()?, + Err(e) => return Err(Box::new(e)), + }; + + if shmem.is_owner() { + //Create an event in the shared memory + println!("Creating event in shared memory"); + let (evt, used_bytes) = unsafe { Event::new(shmem.as_ptr(), true)? }; + println!("\tUsed {} bytes", used_bytes); + + println!("Launch another instance of this example to signal the event !"); + evt.wait(Timeout::Infinite)?; + println!("\tGot signal !"); + } else { + // Open existing event + println!("Openning event from shared memory"); + let (evt, used_bytes) = unsafe { Event::from_existing(shmem.as_ptr())? }; + println!("\tEvent uses {} bytes", used_bytes); + + println!("Signaling event !"); + evt.set(EventState::Signaled)?; + println!("\tSignaled !"); + } + + println!("Done !"); + Ok(()) +} diff --git a/vendor/shared_memory/examples/mutex.rs b/vendor/shared_memory/examples/mutex.rs new file mode 100644 index 0000000..47554bb --- /dev/null +++ b/vendor/shared_memory/examples/mutex.rs @@ -0,0 +1,117 @@ +use std::sync::atomic::{AtomicU8, Ordering}; +use std::thread; + +use clap::Parser; +use raw_sync::locks::*; +use shared_memory::*; + +/// Spawns N threads that increment a value to 10 using a mutex +#[derive(Parser)] +#[clap(author, version, about)] +struct Args { + /// Number of threads to spawn + num_threads: usize, + + /// Count to this value + #[clap(long, short, default_value_t = 50)] + count_to: u8, +} + +fn main() { + env_logger::init(); + let args = Args::parse(); + + if args.num_threads < 1 { + eprintln!("num_threads should be 2 or more"); + return; + } + let mut threads = Vec::with_capacity(args.num_threads); + let _ = std::fs::remove_file("mutex_mapping"); + + // Spawn N threads + for i in 0..args.num_threads { + let thread_id = i + 1; + threads.push(thread::spawn(move || { + increment_value("mutex_mapping", thread_id); + })); + } + + // Wait for threads to exit + for t in threads.drain(..) { + t.join().unwrap(); + } +} + +fn increment_value(shmem_flink: &str, thread_num: usize) { + // Create or open the shared memory mapping + let shmem = match ShmemConf::new().size(4096).flink(shmem_flink).create() { + Ok(m) => m, + Err(ShmemError::LinkExists) => ShmemConf::new().flink(shmem_flink).open().unwrap(), + Err(e) => { + eprintln!( + "Unable to create or open shmem flink {} : {}", + shmem_flink, e + ); + return; + } + }; + + let mut raw_ptr = shmem.as_ptr(); + let is_init: &mut AtomicU8; + + unsafe { + is_init = &mut *(raw_ptr as *mut u8 as *mut AtomicU8); + raw_ptr = raw_ptr.add(8); + }; + + // Initialize or wait for initialized mutex + let mutex = if shmem.is_owner() { + is_init.store(0, Ordering::Relaxed); + // Initialize the mutex + let (lock, _bytes_used) = unsafe { + Mutex::new( + raw_ptr, // Base address of Mutex + raw_ptr.add(Mutex::size_of(Some(raw_ptr))), // Address of data protected by mutex + ) + .unwrap() + }; + is_init.store(1, Ordering::Relaxed); + lock + } else { + // wait until mutex is initialized + while is_init.load(Ordering::Relaxed) != 1 {} + // Load existing mutex + let (lock, _bytes_used) = unsafe { + Mutex::from_existing( + raw_ptr, // Base address of Mutex + raw_ptr.add(Mutex::size_of(Some(raw_ptr))), // Address of data protected by mutex + ) + .unwrap() + }; + lock + }; + + // Loop until mutex data reaches 10 + loop { + // Scope where mutex will be locked + { + let mut guard = mutex.lock().unwrap(); + // Cast mutex data to &mut u8 + let val: &mut u8 = unsafe { &mut **guard }; + if *val > 5 { + println!("[thread#{}] done !", thread_num); + return; + } + + // Print contents and increment value + println!("[thread#{}] Val : {}", thread_num, *val); + *val += 1; + + // Hold lock for a second + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + // Timeout this thread for a second + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} diff --git a/vendor/shared_memory/src/error.rs b/vendor/shared_memory/src/error.rs new file mode 100644 index 0000000..4f5014c --- /dev/null +++ b/vendor/shared_memory/src/error.rs @@ -0,0 +1,48 @@ +#[derive(Debug)] +pub enum ShmemError { + MapSizeZero, + NoLinkOrOsId, + FlinkInvalidOsId, + LinkCreateFailed(std::io::Error), + LinkWriteFailed(std::io::Error), + LinkExists, + LinkOpenFailed(std::io::Error), + LinkReadFailed(std::io::Error), + LinkDoesNotExist, + MappingIdExists, + MapCreateFailed(u32), + MapOpenFailed(u32), + UnknownOsError(u32), +} + +impl std::fmt::Display for ShmemError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ShmemError::MapSizeZero => f.write_str("You cannot create a shared memory mapping of 0 size"), + ShmemError::NoLinkOrOsId => f.write_str("Tried to open mapping without flink path or os_id"), + ShmemError::FlinkInvalidOsId => f.write_str("Tried to open mapping from both flink and os_id but the flink did not point to the same os_id"), + ShmemError::LinkCreateFailed(err) => write!(f, "Creating the link file failed, {}", err), + ShmemError::LinkWriteFailed(err) => write!(f, "Writing the link file failed, {}", err), + ShmemError::LinkExists => f.write_str("Shared memory link already exists"), + ShmemError::LinkOpenFailed(err) => write!(f, "Opening the link file failed, {}", err), + ShmemError::LinkReadFailed(err) => write!(f, "Reading the link file failed, {}", err), + ShmemError::LinkDoesNotExist => f.write_str("Requested link file does not exist"), + ShmemError::MappingIdExists => f.write_str("Shared memory OS specific ID already exists"), + ShmemError::MapCreateFailed(err) => write!(f, "Creating the shared memory failed, os error {}", err), + ShmemError::MapOpenFailed(err) => write!(f, "Opening the shared memory failed, os error {}", err), + ShmemError::UnknownOsError(err) => write!(f, "An unexpected OS error occurred, os error {}", err), + } + } +} + +impl std::error::Error for ShmemError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ShmemError::LinkCreateFailed(err) => Some(err), + ShmemError::LinkWriteFailed(err) => Some(err), + ShmemError::LinkOpenFailed(err) => Some(err), + ShmemError::LinkReadFailed(err) => Some(err), + _ => None, + } + } +} diff --git a/vendor/shared_memory/src/lib.rs b/vendor/shared_memory/src/lib.rs new file mode 100644 index 0000000..2a4126d --- /dev/null +++ b/vendor/shared_memory/src/lib.rs @@ -0,0 +1,267 @@ +//! A thin wrapper around shared memory system calls +//! +//! For help on how to get started, take a look at the [examples](https://github.com/elast0ny/shared_memory-rs/tree/master/examples) ! + +use std::fs::{File, OpenOptions}; +use std::io::{ErrorKind, Read, Write}; + +use std::fs::remove_file; +use std::path::{Path, PathBuf}; + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "logging")] { + pub use log; + } else { + #[allow(unused_macros)] + mod log { + macro_rules! trace (($($tt:tt)*) => {{}}); + macro_rules! debug (($($tt:tt)*) => {{}}); + macro_rules! info (($($tt:tt)*) => {{}}); + macro_rules! warn (($($tt:tt)*) => {{}}); + macro_rules! error (($($tt:tt)*) => {{}}); + pub(crate) use {debug, trace}; + } + } +} + +use crate::log::*; + +mod error; +pub use error::*; + +//Load up the proper OS implementation +cfg_if! { + if #[cfg(target_os="windows")] { + mod windows; + use windows as os_impl; + } else if #[cfg(any(target_os="freebsd", target_os="linux", target_os="macos"))] { + mod unix; + use crate::unix as os_impl; + } else { + compile_error!("shared_memory isnt implemented for this platform..."); + } +} + +#[derive(Clone, Default)] +/// Struct used to configure different parameters before creating a shared memory mapping +pub struct ShmemConf { + owner: bool, + os_id: Option, + overwrite_flink: bool, + flink_path: Option, + size: usize, + ext: os_impl::ShmemConfExt, +} +impl Drop for ShmemConf { + fn drop(&mut self) { + // Delete the flink if we are the owner of the mapping + if self.owner { + if let Some(flink_path) = self.flink_path.as_ref() { + debug!("Deleting file link {}", flink_path.to_string_lossy()); + let _ = remove_file(flink_path); + } + } + } +} + +impl ShmemConf { + /// Create a new default shmem config + pub fn new() -> Self { + ShmemConf::default() + } + /// Provide a specific os identifier for the mapping + /// + /// When not specified, a randomly generated identifier will be used + pub fn os_id>(mut self, os_id: S) -> Self { + self.os_id = Some(String::from(os_id.as_ref())); + self + } + + /// Overwrites file links if it already exist when calling `create()` + pub fn force_create_flink(mut self) -> Self { + self.overwrite_flink = true; + self + } + + /// Create the shared memory mapping with a file link + /// + /// This creates a file on disk that contains the unique os_id for the mapping. + /// This can be useful when application want to rely on filesystems to share mappings + pub fn flink>(mut self, path: S) -> Self { + self.flink_path = Some(PathBuf::from(path.as_ref())); + self + } + + /// Sets the size of the mapping that will be used in `create()` + pub fn size(mut self, size: usize) -> Self { + self.size = size; + self + } + + /// Create a new mapping using the current configuration + pub fn create(mut self) -> Result { + if self.size == 0 { + return Err(ShmemError::MapSizeZero); + } + + if let Some(ref flink_path) = self.flink_path { + if !self.overwrite_flink && flink_path.is_file() { + return Err(ShmemError::LinkExists); + } + } + + // Create the mapping + // Do not generate a random ID if no os_id is set, since we're optimizing for size + let mapping = match self.os_id { + None => return Err(ShmemError::NoLinkOrOsId), + Some(ref specific_id) => os_impl::create_mapping(specific_id, self.size)?, + }; + debug!("Created shared memory mapping '{}'", mapping.unique_id); + + // Create flink + if let Some(ref flink_path) = self.flink_path { + debug!("Creating file link that points to mapping"); + let mut open_options: OpenOptions = OpenOptions::new(); + open_options.write(true); + + if self.overwrite_flink { + open_options.create(true).truncate(true); + } else { + open_options.create_new(true); + } + + match open_options.open(flink_path) { + Ok(mut f) => { + // write the shmem uid asap + if let Err(e) = f.write(mapping.unique_id.as_bytes()) { + let _ = std::fs::remove_file(flink_path); + return Err(ShmemError::LinkWriteFailed(e)); + } + } + Err(e) if e.kind() == ErrorKind::AlreadyExists => { + return Err(ShmemError::LinkExists) + } + Err(e) => return Err(ShmemError::LinkCreateFailed(e)), + } + + debug!( + "Created file link '{}' with id '{}'", + flink_path.to_string_lossy(), + mapping.unique_id + ); + } + + self.owner = true; + self.size = mapping.map_size; + + Ok(Shmem { + config: self, + mapping, + }) + } + + /// Opens an existing mapping using the current configuration + pub fn open(mut self) -> Result { + // Must at least have a flink or an os_id + if self.flink_path.is_none() && self.os_id.is_none() { + debug!("Open called with no file link or unique id..."); + return Err(ShmemError::NoLinkOrOsId); + } + + let mut flink_uid = String::new(); + let mut retry = 0; + loop { + let unique_id = if let Some(ref unique_id) = self.os_id { + retry = 5; + unique_id.as_str() + } else { + let flink_path = self.flink_path.as_ref().unwrap(); + debug!( + "Open shared memory from file link {}", + flink_path.to_string_lossy() + ); + let mut f = match File::open(flink_path) { + Ok(f) => f, + Err(e) => return Err(ShmemError::LinkOpenFailed(e)), + }; + flink_uid.clear(); + if let Err(e) = f.read_to_string(&mut flink_uid) { + return Err(ShmemError::LinkReadFailed(e)); + } + flink_uid.as_str() + }; + + match os_impl::open_mapping(unique_id, self.size, &self.ext) { + Ok(m) => { + self.size = m.map_size; + self.owner = false; + + return Ok(Shmem { + config: self, + mapping: m, + }); + } + // If we got this failing os_id from the flink, try again in case the shmem owner didnt write the full + // unique_id to the file + Err(ShmemError::MapOpenFailed(_)) if self.os_id.is_none() && retry < 5 => { + retry += 1; + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => return Err(e), + } + } + } +} + +/// Structure used to extract information from an existing shared memory mapping +pub struct Shmem { + config: ShmemConf, + mapping: os_impl::MapData, +} +#[allow(clippy::len_without_is_empty)] +impl Shmem { + /// Returns whether we created the mapping or not + pub fn is_owner(&self) -> bool { + self.config.owner + } + /// Allows for gaining/releasing ownership of the mapping + /// + /// Warning : You must ensure at least one process owns the mapping in order to ensure proper cleanup code is ran + pub fn set_owner(&mut self, is_owner: bool) -> bool { + self.mapping.set_owner(is_owner); + + let prev_val = self.config.owner; + self.config.owner = is_owner; + prev_val + } + /// Returns the OS unique identifier for the mapping + pub fn get_os_id(&self) -> &str { + self.mapping.unique_id.as_str() + } + /// Returns the flink path if present + pub fn get_flink_path(&self) -> Option<&PathBuf> { + self.config.flink_path.as_ref() + } + /// Returns the total size of the mapping + pub fn len(&self) -> usize { + self.mapping.map_size + } + /// Returns a raw pointer to the mapping + pub fn as_ptr(&self) -> *mut u8 { + self.mapping.as_mut_ptr() + } + /// Returns mapping as a byte slice + /// # Safety + /// This function is unsafe because it is impossible to ensure the range of bytes is immutable + pub unsafe fn as_slice(&self) -> &[u8] { + std::slice::from_raw_parts(self.as_ptr(), self.len()) + } + /// Returns mapping as a mutable byte slice + /// # Safety + /// This function is unsafe because it is impossible to ensure the returned mutable refence is unique/exclusive + pub unsafe fn as_slice_mut(&mut self) -> &mut [u8] { + std::slice::from_raw_parts_mut(self.as_ptr(), self.len()) + } +} diff --git a/vendor/shared_memory/src/unix.rs b/vendor/shared_memory/src/unix.rs new file mode 100644 index 0000000..695b5f5 --- /dev/null +++ b/vendor/shared_memory/src/unix.rs @@ -0,0 +1,217 @@ +use std::os::unix::io::RawFd; +use std::ptr::null_mut; + +use crate::log::*; +use nix::fcntl::OFlag; +use nix::sys::mman::{mmap, munmap, shm_open, shm_unlink, MapFlags, ProtFlags}; +use nix::sys::stat::{fstat, Mode}; +use nix::unistd::{close, ftruncate}; + +use crate::ShmemError; + +#[derive(Clone, Default)] +pub struct ShmemConfExt; + +pub struct MapData { + //On linux, you must shm_unlink() the object created for the mapping. It wont disappear automatically. + owner: bool, + + //File descriptor to our open mapping + map_fd: RawFd, + + //Shared mapping uid + pub unique_id: String, + //Total size of the mapping + pub map_size: usize, + //Pointer to the first address of our mapping + pub map_ptr: *mut u8, +} + +impl MapData { + pub fn as_mut_ptr(&self) -> *mut u8 { + self.map_ptr + } +} + +/// Shared memory teardown for linux +impl Drop for MapData { + ///Takes care of properly closing the SharedMem (munmap(), shmem_unlink(), close()) + fn drop(&mut self) { + //Unmap memory + if !self.map_ptr.is_null() { + trace!( + "munmap(map_ptr:{:p},map_size:{})", + self.map_ptr, + self.map_size + ); + if let Err(_e) = unsafe { munmap(self.map_ptr as *mut _, self.map_size) } { + debug!("Failed to munmap() shared memory mapping : {}", _e); + }; + } + + //Unlink shmem + if self.map_fd != 0 { + //unlink shmem if we created it + if self.owner { + debug!("Deleting persistent mapping"); + trace!("shm_unlink({})", self.unique_id.as_str()); + if let Err(_e) = shm_unlink(self.unique_id.as_str()) { + debug!("Failed to shm_unlink() shared memory : {}", _e); + }; + } + + trace!("close({})", self.map_fd); + if let Err(_e) = close(self.map_fd) { + debug!( + "os_impl::Linux : Failed to close() shared memory file descriptor : {}", + _e + ); + }; + } + } +} + +impl MapData { + pub fn set_owner(&mut self, is_owner: bool) -> bool { + let prev_val = self.owner; + self.owner = is_owner; + prev_val + } +} + +/// Creates a mapping specified by the uid and size +pub fn create_mapping(unique_id: &str, map_size: usize) -> Result { + //Create shared memory file descriptor + debug!("Creating persistent mapping at {}", unique_id); + let shmem_fd = match shm_open( + unique_id, //Unique name that usualy pops up in /dev/shm/ + OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_RDWR, //create exclusively (error if collision) and read/write to allow resize + Mode::S_IRUSR | Mode::S_IWUSR, //Permission allow user+rw + ) { + Ok(v) => { + trace!( + "shm_open({}, {:X}, {:X}) == {}", + unique_id, + OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_RDWR, + Mode::S_IRUSR | Mode::S_IWUSR, + v + ); + v + } + Err(nix::Error::EEXIST) => return Err(ShmemError::MappingIdExists), + Err(e) => return Err(ShmemError::MapCreateFailed(e as u32)), + }; + + let mut new_map: MapData = MapData { + owner: true, + unique_id: String::from(unique_id), + map_fd: shmem_fd, + map_size, + map_ptr: null_mut(), + }; + + //Enlarge the memory descriptor file size to the requested map size + debug!("Creating memory mapping"); + trace!("ftruncate({}, {})", new_map.map_fd, new_map.map_size); + match ftruncate(new_map.map_fd, new_map.map_size as _) { + Ok(_) => {} + Err(e) => return Err(ShmemError::UnknownOsError(e as u32)), + }; + + //Put the mapping in our address space + debug!("Loading mapping into address space"); + new_map.map_ptr = match unsafe { + mmap( + null_mut(), //Desired addr + new_map.map_size, //size of mapping + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, //Permissions on pages + MapFlags::MAP_SHARED, //What kind of mapping + new_map.map_fd, //fd + 0, //Offset into fd + ) + } { + Ok(v) => { + trace!( + "mmap(NULL, {}, {:X}, {:X}, {}, 0) == {:p}", + new_map.map_size, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_SHARED, + new_map.map_fd, + v + ); + v as *mut _ + } + Err(e) => return Err(ShmemError::MapCreateFailed(e as u32)), + }; + + Ok(new_map) +} + +/// Opens an existing mapping specified by its uid +pub fn open_mapping( + unique_id: &str, + _map_size: usize, + _ext: &ShmemConfExt, +) -> Result { + //Open shared memory + debug!("Openning persistent mapping at {}", unique_id); + let shmem_fd = match shm_open( + unique_id, + OFlag::O_RDWR, //Open read write + Mode::S_IRUSR, + ) { + Ok(v) => { + trace!( + "shm_open({}, {:X}, {:X}) == {}", + unique_id, + OFlag::O_RDWR, + Mode::S_IRUSR, + v + ); + v + } + Err(e) => return Err(ShmemError::MapOpenFailed(e as u32)), + }; + + let mut new_map: MapData = MapData { + owner: false, + unique_id: String::from(unique_id), + map_fd: shmem_fd, + map_size: 0, + map_ptr: null_mut(), + }; + + //Get mmap size + new_map.map_size = match fstat(new_map.map_fd) { + Ok(v) => v.st_size as usize, + Err(e) => return Err(ShmemError::MapOpenFailed(e as u32)), + }; + + //Map memory into our address space + debug!("Loading mapping into address space"); + new_map.map_ptr = match unsafe { + mmap( + null_mut(), //Desired addr + new_map.map_size, //size of mapping + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, //Permissions on pages + MapFlags::MAP_SHARED, //What kind of mapping + new_map.map_fd, //fd + 0, //Offset into fd + ) + } { + Ok(v) => { + trace!( + "mmap(NULL, {}, {:X}, {:X}, {}, 0) == {:p}", + new_map.map_size, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_SHARED, + new_map.map_fd, + v + ); + v as *mut _ + } + Err(e) => return Err(ShmemError::MapOpenFailed(e as u32)), + }; + + Ok(new_map) +} diff --git a/vendor/shared_memory/src/windows.rs b/vendor/shared_memory/src/windows.rs new file mode 100644 index 0000000..96f4a59 --- /dev/null +++ b/vendor/shared_memory/src/windows.rs @@ -0,0 +1,281 @@ +use std::fs::{File, OpenOptions}; +use std::io::ErrorKind; +use std::os::windows::{fs::OpenOptionsExt, io::AsRawHandle}; +use std::path::PathBuf; + +use crate::{log::*, ShmemConf}; +use win_sys::*; + +use crate::ShmemError; + +#[derive(Clone, Default)] +pub struct ShmemConfExt { + allow_raw: bool, +} + +impl ShmemConf { + /// If set to true, enables openning raw shared memory that is not managed by this crate + pub fn allow_raw(mut self, allow: bool) -> Self { + self.ext.allow_raw = allow; + self + } +} + +pub struct MapData { + owner: bool, + + /// Pointer to the first byte of our mapping + /// Keep this above `file_map` so it gets dropped first + pub view: ViewOfFile, + + /// The handle to our open mapping + #[allow(dead_code)] + file_map: FileMapping, + + /// This file is used for shmem persistence. When an owner wants to drop the mapping, + /// it opens the file with FILE_FLAG_DELETE_ON_CLOSE, renames the file and closes it. + /// This makes it so future calls to open the old mapping will fail (as it was renamed) and + /// deletes the renamed file once all handles have been closed. + #[allow(dead_code)] + persistent_file: Option, + + //Shared mapping uid + pub unique_id: String, + //Total size of the mapping + pub map_size: usize, +} +///Teardown UnmapViewOfFile and close CreateMapping handle +impl Drop for MapData { + ///Takes care of properly closing the SharedMem + fn drop(&mut self) { + // Inspired by the boost implementation at + // https://github.com/boostorg/interprocess/blob/140b50efb3281fa3898f3a4cf939cfbda174718f/include/boost/interprocess/detail/win32_api.hpp + // Emulate POSIX behavior by + // 1. Opening the mmapped file with `FILE_FLAG_DELETE_ON_CLOSE`, causing it to be + // deleted when all its handles have been closed. + // 2. Renaming the mmapped file to prevent future access/opening. + // Once this has run, existing file/mapping handles remain usable but the file is + // deleted once all handles have been closed and no new handles can be opened + // because the file has been renamed. This matches the behavior of shm_unlink() + // on unix. + if self.owner { + let mut base_path = get_tmp_dir().unwrap(); + + // 1. Set file attributes so that it deletes itself once everyone has closed it + let file_path = base_path.join(self.unique_id.trim_start_matches('/')); + debug!("Setting mapping to delete after everyone has closed it"); + match OpenOptions::new() + .access_mode(GENERIC_READ | GENERIC_WRITE | DELETE) + .share_mode((FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE).0) + .create(false) + .attributes((FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE).0) + .open(&file_path) + { + Ok(_) => { + // 2. Rename file to prevent further use + base_path.push(&format!( + "{}_deleted", + self.unique_id.trim_start_matches('/') + )); + debug!( + "Renaming {} to {}", + file_path.to_string_lossy(), + base_path.to_string_lossy() + ); + if let Err(_e) = std::fs::rename(&file_path, &base_path) { + debug!( + "Failed to rename persistent_file {} : {}", + file_path.to_string_lossy(), + _e + ); + } + } + Err(_e) => { + debug!( + "Failed to set DELETE_ON_CLOSE on persistent_file {} : {}", + file_path.to_string_lossy(), + _e + ); + } + }; + } + } +} + +impl MapData { + pub fn set_owner(&mut self, is_owner: bool) -> bool { + let prev_val = self.owner; + self.owner = is_owner; + prev_val + } + pub fn as_mut_ptr(&self) -> *mut u8 { + self.view.as_mut_ptr() as _ + } +} + +/// Returns the path to a temporary directory in which to store files backing the shared memory. If it +/// doesn't exist, the directory is created. +fn get_tmp_dir() -> Result { + debug!("Getting & creating shared_memory-rs temp dir"); + let mut path = std::env::temp_dir(); + path.push("shared_memory-rs"); + + if path.is_dir() { + return Ok(path); + } + + match std::fs::create_dir_all(path.as_path()) { + Ok(_) => Ok(path), + Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(path), + Err(e) => Err(ShmemError::UnknownOsError(e.raw_os_error().unwrap() as _)), + } +} + +fn new_map( + unique_id: &str, + mut map_size: usize, + create: bool, + allow_raw: bool, +) -> Result { + // Create file to back the shared memory + let mut file_path = get_tmp_dir()?; + file_path.push(unique_id.trim_start_matches('/')); + debug!( + "{} persistent_file at {}", + if create { "Creating" } else { "Openning" }, + file_path.to_string_lossy() + ); + + let mut opt = OpenOptions::new(); + opt.read(true) + .write(true) + .share_mode((FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE).0) + .attributes((FILE_ATTRIBUTE_TEMPORARY).0); + if create { + opt.create_new(true); + } else { + opt.create(false); + }; + + let mut persistent_file = None; + let map_h = match opt.open(&file_path) { + Ok(f) => { + //Create/open Mapping using persistent file + debug!( + "{} memory mapping", + if create { "Creating" } else { "Openning" }, + ); + let high_size: u32 = ((map_size as u64 & 0xFFFF_FFFF_0000_0000_u64) >> 32) as u32; + let low_size: u32 = (map_size as u64 & 0xFFFF_FFFF_u64) as u32; + trace!( + "CreateFileMapping({:?}, NULL, {:X}, {}, {}, '{}')", + HANDLE(f.as_raw_handle() as _), + PAGE_READWRITE.0, + high_size, + low_size, + unique_id, + ); + + match CreateFileMapping( + HANDLE(f.as_raw_handle() as _), + None, + PAGE_READWRITE, + high_size, + low_size, + unique_id, + ) { + Ok(v) => { + persistent_file = Some(f); + v + } + Err(e) => { + let err_code = e.win32_error().unwrap(); + return if err_code == ERROR_ALREADY_EXISTS { + Err(ShmemError::MappingIdExists) + } else { + Err(if create { + ShmemError::MapCreateFailed(err_code.0) + } else { + ShmemError::MapOpenFailed(err_code.0) + }) + }; + } + } + } + Err(e) if e.kind() == ErrorKind::AlreadyExists => return Err(ShmemError::MappingIdExists), + Err(e) => { + if create { + return Err(ShmemError::MapCreateFailed(e.raw_os_error().unwrap() as _)); + } else if !allow_raw { + return Err(ShmemError::MapOpenFailed(ERROR_FILE_NOT_FOUND.0)); + } + + // This may be a mapping that isnt managed by this crate + // Try to open the mapping without any backing file + trace!( + "OpenFileMappingW({:?}, {}, '{}')", + FILE_MAP_ALL_ACCESS, + false, + unique_id, + ); + match OpenFileMapping(FILE_MAP_ALL_ACCESS, false, unique_id) { + Ok(h) => h, + Err(e) => { + return Err(ShmemError::MapOpenFailed(e.win32_error().unwrap().0)); + } + } + } + }; + trace!("0x{:X}", map_h); + + //Map mapping into address space + debug!("Loading mapping into address space"); + trace!( + "MapViewOfFile(0x{:X}, {:X}, 0, 0, 0)", + map_h, + (FILE_MAP_READ | FILE_MAP_WRITE).0, + ); + let map_ptr = match MapViewOfFile(map_h.as_handle(), FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0) { + Ok(v) => v, + Err(e) => { + return Err(if create { + ShmemError::MapCreateFailed(e.win32_error().unwrap().0) + } else { + ShmemError::MapOpenFailed(e.win32_error().unwrap().0) + }) + } + }; + trace!("\t{:p}", map_ptr); + + if !create { + //Get the real size of the openned mapping + let mut info = MEMORY_BASIC_INFORMATION::default(); + if let Err(e) = VirtualQuery(map_ptr.as_mut_ptr(), &mut info) { + return Err(ShmemError::UnknownOsError(e.win32_error().unwrap().0)); + } + map_size = info.RegionSize; + } + + Ok(MapData { + owner: create, + file_map: map_h, + persistent_file, + unique_id: unique_id.to_string(), + map_size, + view: map_ptr, + }) +} + +//Creates a mapping specified by the uid and size +pub fn create_mapping(unique_id: &str, map_size: usize) -> Result { + new_map(unique_id, map_size, true, false) +} + +//Opens an existing mapping specified by its uid +pub fn open_mapping( + unique_id: &str, + map_size: usize, + ext: &ShmemConfExt, +) -> Result { + new_map(unique_id, map_size, false, ext.allow_raw) +} diff --git a/vendor/shared_memory/tests/general.rs b/vendor/shared_memory/tests/general.rs new file mode 100644 index 0000000..2812711 --- /dev/null +++ b/vendor/shared_memory/tests/general.rs @@ -0,0 +1,117 @@ +use std::path::Path; + +use shared_memory::ShmemConf; + +#[test] +fn create_new() { + let mut s = ShmemConf::new().size(4090).create().unwrap(); + + assert!(s.is_owner()); + assert!(!s.get_os_id().is_empty()); + assert!(s.len() >= 4090); + assert!(!s.as_ptr().is_null()); + unsafe { + assert_eq!(s.as_slice().len(), s.len()); + assert_eq!(s.as_slice_mut().len(), s.len()); + } +} + +#[test] +fn create_with_flink() { + let flink = Path::new("create_new1"); + + let mut s = ShmemConf::new().flink(flink).size(4090).create().unwrap(); + + assert!(s.is_owner()); + assert!(!s.get_os_id().is_empty()); + assert!(flink.is_file()); + assert!(s.len() >= 4090); + assert!(!s.as_ptr().is_null()); + unsafe { + assert_eq!(s.as_slice().len(), s.len()); + assert_eq!(s.as_slice_mut().len(), s.len()); + } + + drop(s); + + assert!(!flink.is_file()); +} + +#[test] +fn open_os_id() { + let s1 = ShmemConf::new().size(4090).create().unwrap(); + + // Open with the unique os id + let os_id = s1.get_os_id().to_string(); + let mut s2 = ShmemConf::new().os_id(&os_id).open().unwrap(); + + assert!(!s2.is_owner()); + assert!(!s2.get_os_id().is_empty()); + assert!(s2.len() >= 4090); + assert!(!s2.as_ptr().is_null()); + unsafe { + assert_eq!(s2.as_slice().len(), s2.len()); + assert_eq!(s2.as_slice_mut().len(), s2.len()); + } + + // Drop the owner of the mapping + drop(s1); + + // Make sure it can be openned again + assert!(ShmemConf::new().os_id(&os_id).open().is_err()); + + drop(s2); +} + +#[test] +fn open_flink() { + let flink = Path::new("create_new2"); + let s1 = ShmemConf::new().flink(flink).size(4090).create().unwrap(); + + // Open with file base link + let mut s2 = ShmemConf::new().flink(&flink).open().unwrap(); + + assert!(!s2.is_owner()); + assert!(!s2.get_os_id().is_empty()); + assert!(flink.is_file()); + assert!(s2.len() >= 4090); + assert!(!s2.as_ptr().is_null()); + unsafe { + assert_eq!(s2.as_slice().len(), s2.len()); + assert_eq!(s2.as_slice_mut().len(), s2.len()); + } + + // Drop the owner of the mapping + drop(s1); + + // Make sure it can be openned again + assert!(ShmemConf::new().flink(&flink).open().is_err()); + + drop(s2); +} + +#[test] +fn share_data() { + let s1 = ShmemConf::new() + .size(core::mem::size_of::()) + .create() + .unwrap(); + + // Open with the unique os id + let os_id = s1.get_os_id().to_string(); + let s2 = ShmemConf::new().os_id(&os_id).open().unwrap(); + + let ptr1 = s1.as_ptr() as *mut u32; + let ptr2 = s2.as_ptr() as *mut u32; + + // Confirm that the two pointers are different + assert_ne!(ptr1, ptr2); + + // Write a value from s1 and read it from s2 + unsafe { + let shared_val = 0xBADC0FEE; + ptr1.write_volatile(shared_val); + let read_val = ptr2.read_volatile(); + assert_eq!(read_val, shared_val); + } +} diff --git a/vendor/shared_memory/tests/posix_semantics.rs b/vendor/shared_memory/tests/posix_semantics.rs new file mode 100644 index 0000000..b48cd13 --- /dev/null +++ b/vendor/shared_memory/tests/posix_semantics.rs @@ -0,0 +1,90 @@ +use shared_memory::ShmemConf; +use std::sync::mpsc::channel; +use std::thread; + +#[test] +fn persistence() { + let os_id = { + let mut shmem = ShmemConf::new().size(4096).create().unwrap(); + shmem.set_owner(false); + String::from(shmem.get_os_id()) + }; + let mut shmem = ShmemConf::new().os_id(os_id).open().unwrap(); + shmem.set_owner(true); +} + +#[test] +fn posix_behavior() { + let (tx_a, rx_a) = channel(); + let (tx_b, rx_b) = channel(); + let (tx_c, rx_c) = channel(); + + let thread_a = thread::Builder::new() + .name(String::from("A")) + .spawn(move || { + let os_id = { + let shmem = ShmemConf::new().size(4096).create().unwrap(); + let os_id = String::from(shmem.get_os_id()); + // Creating two `Shmem`s with the same `os_id` should fail + assert!(ShmemConf::new().size(4096).os_id(&os_id).create().is_err()); + tx_b.send(os_id.clone()).unwrap(); + tx_c.send(os_id.clone()).unwrap(); + // Wait for threads B and C to confirm they have created their instances. + rx_a.recv().unwrap(); + rx_a.recv().unwrap(); + // Tell thread B to drop its instance. + tx_b.send(String::new()).unwrap(); + os_id + // Owned shmem drops here after a second owned instance has been + // dropped in thread B. + }; + // Should not be able to reopen shared memory after an owned instance + // has been dropped in thread B. + assert!(ShmemConf::new().size(4096).os_id(os_id).open().is_err()); + // Tell thread C to drop the unowned instance. + tx_c.send(String::new()).unwrap(); + }) + .unwrap(); + let thread_b = thread::Builder::new() + .name(String::from("B")) + .spawn({ + let tx_a = tx_a.clone(); + move || { + let existing_os_id = rx_b.recv().unwrap(); + // Creating two `Shmem`s with the same `os_id` should fail + assert!(ShmemConf::new() + .size(4096) + .os_id(&existing_os_id) + .create() + .is_err()); + { + // Should be able to open the existing shared memory + let mut shmem = ShmemConf::new().os_id(&existing_os_id).open().unwrap(); + shmem.set_owner(true); + tx_a.send(String::new()).unwrap(); + rx_b.recv().unwrap(); + // When the owning shmem is dropped here, we + // 1. should be able to still drop the original shared memory in thread A. + // 2. should not be able to reopen it with the same name in thread A, even + // if an instance is kept alive in thread C. + } + } + }) + .unwrap(); + let thread_c = thread::Builder::new() + .name(String::from("C")) + .spawn(move || { + // This thread keeps a shared memory instance alive until it's told to + // drop it. + let existing_os_id = rx_c.recv().unwrap(); + let _shmem = ShmemConf::new().os_id(&existing_os_id).open().unwrap(); + // Indicate to thread A that the instance has been created. + tx_a.send(String::new()).unwrap(); + // Shut down signal. + rx_c.recv().unwrap(); + }) + .unwrap(); + thread_a.join().unwrap(); + thread_b.join().unwrap(); + thread_c.join().unwrap(); +}