Buttplug sex toy control library
at dev 9.0 kB view raw
1#[macro_use] 2extern crate log; 3 4use argh::FromArgs; 5use getset::{CopyGetters, Getters}; 6use intiface_engine::{ 7 EngineOptions, EngineOptionsBuilder, IntifaceEngine, IntifaceEngineError, IntifaceError, 8}; 9use std::fs; 10use tokio::{select, signal::ctrl_c}; 11use tracing::Level; 12use tracing_subscriber::{ 13 filter::{EnvFilter, LevelFilter}, 14 layer::SubscriberExt, 15 util::SubscriberInitExt, 16}; 17 18const VERSION: &str = env!("CARGO_PKG_VERSION"); 19 20/// command line interface for intiface/buttplug. 21/// 22/// Note: Commands are one word to keep compat with C#/JS executables currently. 23#[derive(FromArgs, Getters, CopyGetters)] 24pub struct IntifaceCLIArguments { 25 // Options that do something then exit 26 /// print version and exit. 27 #[argh(switch)] 28 #[getset(get_copy = "pub")] 29 version: bool, 30 31 /// print version and exit. 32 #[argh(switch)] 33 #[getset(get_copy = "pub")] 34 server_version: bool, 35 36 // Options that set up the server networking 37 /// if passed, websocket server listens on all interfaces. Otherwise, only 38 /// listen on 127.0.0.1. 39 #[argh(switch)] 40 #[getset(get_copy = "pub")] 41 websocket_use_all_interfaces: bool, 42 43 /// insecure port for websocket servers. 44 #[argh(option)] 45 #[getset(get_copy = "pub")] 46 websocket_port: Option<u16>, 47 48 /// insecure address for connecting to websocket servers. 49 #[argh(option)] 50 #[getset(get = "pub")] 51 websocket_client_address: Option<String>, 52 53 // Options that set up communications with intiface GUI 54 /// if passed, output json for parent process via websockets 55 #[argh(option)] 56 #[getset(get_copy = "pub")] 57 frontend_websocket_port: Option<u16>, 58 59 // Options that set up Buttplug server parameters 60 /// name of server to pass to connecting clients. 61 #[argh(option)] 62 #[argh(default = "\"Buttplug Server\".to_owned()")] 63 #[getset(get = "pub")] 64 server_name: String, 65 66 /// path to the device configuration file 67 #[argh(option)] 68 #[getset(get = "pub")] 69 device_config_file: Option<String>, 70 71 /// path to user device configuration file 72 #[argh(option)] 73 #[getset(get = "pub")] 74 user_device_config_file: Option<String>, 75 76 /// ping timeout maximum for server (in milliseconds) 77 #[argh(option)] 78 #[argh(default = "0")] 79 #[getset(get_copy = "pub")] 80 max_ping_time: u32, 81 82 /// set log level for output 83 #[allow(dead_code)] 84 #[argh(option)] 85 #[getset(get_copy = "pub")] 86 log: Option<Level>, 87 88 /// turn off bluetooth le device support 89 #[argh(switch)] 90 #[getset(get_copy = "pub")] 91 use_bluetooth_le: bool, 92 93 /// turn off serial device support 94 #[argh(switch)] 95 #[getset(get_copy = "pub")] 96 use_serial: bool, 97 98 /// turn off hid device support 99 #[allow(dead_code)] 100 #[argh(switch)] 101 #[getset(get_copy = "pub")] 102 use_hid: bool, 103 104 /// turn off lovense dongle serial device support 105 #[argh(switch)] 106 #[getset(get_copy = "pub")] 107 use_lovense_dongle_serial: bool, 108 109 /// turn off lovense dongle hid device support 110 #[argh(switch)] 111 #[getset(get_copy = "pub")] 112 use_lovense_dongle_hid: bool, 113 114 /// turn off xinput gamepad device support (windows only) 115 #[argh(switch)] 116 #[getset(get_copy = "pub")] 117 use_xinput: bool, 118 119 /// turn on lovense connect app device support (off by default) 120 #[argh(switch)] 121 #[getset(get_copy = "pub")] 122 use_lovense_connect: bool, 123 124 /// turn on websocket server device comm manager 125 #[argh(switch)] 126 #[getset(get_copy = "pub")] 127 use_device_websocket_server: bool, 128 129 /// port for device websocket server comm manager (defaults to 54817) 130 #[argh(option)] 131 #[getset(get_copy = "pub")] 132 device_websocket_server_port: Option<u16>, 133 134 /// if set, broadcast server port/service info via mdns 135 #[argh(switch)] 136 #[getset(get_copy = "pub")] 137 broadcast_server_mdns: bool, 138 139 /// mdns suffix, will be appended to instance names for advertised mdns services (optional, ignored if broadcast_mdns is not set) 140 #[argh(option)] 141 #[getset(get = "pub")] 142 mdns_suffix: Option<String>, 143 144 /// if set, use repeater mode instead of engine mode 145 #[argh(switch)] 146 #[getset(get_copy = "pub")] 147 repeater: bool, 148 149 /// if set, use repeater mode instead of engine mode 150 #[argh(option)] 151 #[getset(get_copy = "pub")] 152 repeater_port: Option<u16>, 153 154 /// if set, use rest api instead of bringing up server 155 #[argh(option)] 156 #[getset(get = "pub")] 157 rest_api_port: Option<u16>, 158 159 #[cfg(debug_assertions)] 160 /// crash the main thread (that holds the runtime) 161 #[argh(switch)] 162 #[getset(get_copy = "pub")] 163 crash_main_thread: bool, 164 165 #[allow(dead_code)] 166 #[cfg(debug_assertions)] 167 /// crash the task thread (for testing logging/reporting) 168 #[argh(switch)] 169 #[getset(get_copy = "pub")] 170 crash_task_thread: bool, 171} 172 173pub fn setup_console_logging(log_level: Option<Level>) { 174 if log_level.is_some() { 175 tracing_subscriber::registry() 176 .with(tracing_subscriber::fmt::layer()) 177 .with(LevelFilter::from(log_level)) 178 .try_init() 179 .unwrap(); 180 } else { 181 tracing_subscriber::registry() 182 .with(tracing_subscriber::fmt::layer()) 183 .with( 184 EnvFilter::try_from_default_env() 185 .or_else(|_| EnvFilter::try_new("info")) 186 .unwrap(), 187 ) 188 .try_init() 189 .unwrap(); 190 }; 191 println!("Intiface Server, starting up with stdout output."); 192} 193 194impl TryFrom<IntifaceCLIArguments> for EngineOptions { 195 type Error = IntifaceError; 196 fn try_from(args: IntifaceCLIArguments) -> Result<Self, IntifaceError> { 197 let mut builder = EngineOptionsBuilder::default(); 198 199 if let Some(deviceconfig) = args.device_config_file() { 200 info!( 201 "Intiface CLI Options: External Device Config {}", 202 deviceconfig 203 ); 204 match fs::read_to_string(deviceconfig) { 205 Ok(cfg) => builder.device_config_json(&cfg), 206 Err(err) => { 207 return Err(IntifaceError::new(&format!( 208 "Error opening external device configuration: {:?}", 209 err 210 ))); 211 } 212 }; 213 } 214 215 if let Some(userdeviceconfig) = args.user_device_config_file() { 216 info!( 217 "Intiface CLI Options: User Device Config {}", 218 userdeviceconfig 219 ); 220 builder.user_device_config_path(userdeviceconfig); 221 match fs::read_to_string(userdeviceconfig) { 222 Ok(cfg) => { 223 builder.user_device_config_json(&cfg); 224 } 225 Err(err) => { 226 warn!( 227 "Error opening user device configuration, ignoring and creating new file: {:?}", 228 err 229 ); 230 } 231 }; 232 } 233 234 builder 235 .websocket_use_all_interfaces(args.websocket_use_all_interfaces()) 236 .use_bluetooth_le(args.use_bluetooth_le()) 237 .use_serial_port(args.use_serial()) 238 .use_hid(args.use_hid()) 239 .use_lovense_dongle_serial(args.use_lovense_dongle_serial()) 240 .use_lovense_dongle_hid(args.use_lovense_dongle_hid()) 241 .use_xinput(args.use_xinput()) 242 .use_lovense_connect(args.use_lovense_connect()) 243 .use_device_websocket_server(args.use_device_websocket_server()) 244 .max_ping_time(args.max_ping_time()) 245 .server_name(args.server_name()) 246 .broadcast_server_mdns(args.broadcast_server_mdns()); 247 248 #[cfg(debug_assertions)] 249 { 250 builder 251 .crash_main_thread(args.crash_main_thread()) 252 .crash_task_thread(args.crash_task_thread()); 253 } 254 255 if let Some(value) = args.websocket_port() { 256 builder.websocket_port(value); 257 } 258 if let Some(value) = args.websocket_client_address() { 259 builder.websocket_client_address(value); 260 } 261 if let Some(value) = args.frontend_websocket_port() { 262 builder.frontend_websocket_port(value); 263 } 264 if let Some(value) = args.device_websocket_server_port() { 265 builder.device_websocket_server_port(value); 266 } 267 if let Some(value) = args.rest_api_port() { 268 builder.rest_api_port(*value); 269 } 270 if args.broadcast_server_mdns() 271 && let Some(value) = args.mdns_suffix() { 272 builder.mdns_suffix(value); 273 } 274 Ok(builder.finish()) 275 } 276} 277 278#[tokio::main(flavor = "current_thread")] //#[tokio::main] 279async fn main() -> Result<(), IntifaceEngineError> { 280 let args: IntifaceCLIArguments = argh::from_env(); 281 if args.server_version() { 282 println!("{}", VERSION); 283 return Ok(()); 284 } 285 286 if args.version() { 287 debug!("Server version command sent, printing and exiting."); 288 println!( 289 "Intiface CLI (Rust Edition) Version {}, Commit {}, Built {}", 290 VERSION, 291 option_env!("VERGEN_GIT_SHA_SHORT").unwrap_or("unknown"), 292 option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown") 293 ); 294 return Ok(()); 295 } 296 297 if args.frontend_websocket_port().is_none() { 298 setup_console_logging(args.log()); 299 } 300 301 let options = EngineOptions::try_from(args).map_err(IntifaceEngineError::from)?; 302 let engine = IntifaceEngine::default(); 303 select! { 304 result = engine.run(&options, None, &None) => { 305 if let Err(e) = result { 306 println!("Server errored while running:"); 307 println!("{:?}", e); 308 } 309 } 310 _ = ctrl_c() => { 311 info!("Control-c hit, exiting."); 312 engine.stop(); 313 } 314 } 315 316 Ok(()) 317}