Buttplug sex toy control library

feat: Move intiface-engine back into main buttplug repo

It's just a library/executable frontend at this point, no reason to
maintain seperately. We probably need a releases site for it but that's
doable.

Fixes #739

+2815
+1
Cargo.toml
··· 16 16 "crates/buttplug_server_hwmgr_xinput", 17 17 "crates/buttplug_tests", 18 18 "crates/buttplug_transport_websocket_tungstenite", 19 + "crates/intiface_engine", 19 20 ] 20 21 21 22 [profile.release]
+1
crates/intiface_engine/.github/workflows/cache_version
··· 1 + 3
+154
crates/intiface_engine/.github/workflows/rust.yml
··· 1 + name: Intiface Engine Build 2 + 3 + on: 4 + push: 5 + branches: 6 + - main 7 + - dev 8 + - ci 9 + 10 + concurrency: 11 + group: ${{ github.head_ref || github.ref }} 12 + cancel-in-progress: true 13 + 14 + jobs: 15 + build-stable: 16 + runs-on: ${{ matrix.os }} 17 + strategy: 18 + matrix: 19 + os: [ubuntu-latest, macos-latest, windows-latest] 20 + steps: 21 + - uses: actions/checkout@v2 22 + - name: Fix ~/.cargo directory permissions 23 + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') 24 + run: sudo chown -R $(whoami):$(id -ng) ~/.cargo/ 25 + - name: Update package list 26 + if: startsWith(matrix.os, 'ubuntu') 27 + run: sudo apt-get -y update 28 + - name: Install required packages 29 + if: startsWith(matrix.os, 'ubuntu') 30 + run: sudo apt-get -y install libudev-dev libusb-1.0-0-dev libdbus-1-dev 31 + - name: Cache cargo registry 32 + uses: actions/cache@v1 33 + with: 34 + path: ~/.cargo/registry 35 + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} 36 + - name: Cache cargo build 37 + uses: actions/cache@v1 38 + with: 39 + path: target 40 + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} 41 + - name: Rust toolchain fetch 42 + uses: actions-rs/toolchain@v1 43 + with: 44 + profile: minimal 45 + toolchain: nightly 46 + override: true 47 + components: rustfmt, clippy 48 + - name: Formatting check 49 + continue-on-error: true 50 + uses: actions-rs/cargo@v1 51 + with: 52 + command: fmt 53 + args: --all -- --check 54 + - name: Build Release 55 + run: cargo build --release 56 + - name: Copy executable (Linux, MacOS) 57 + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') 58 + run: | 59 + mkdir ci-output-release 60 + cp target/release/intiface-engine ci-output-release/intiface-engine 61 + - name: Copy executable (Windows) 62 + if: startsWith(matrix.os, 'windows') 63 + run: | 64 + mkdir ci-output-release 65 + copy target\release\intiface-engine.exe ci-output-release\intiface-engine.exe 66 + - name: Upload artifacts (release) 67 + uses: actions/upload-artifact@v4 68 + with: 69 + name: intiface-engine-${{ runner.os }}-release 70 + path: ci-output-release 71 + build-v4: 72 + runs-on: windows-latest 73 + steps: 74 + - uses: actions/checkout@v2 75 + - name: Cache cargo registry 76 + uses: actions/cache@v1 77 + with: 78 + path: ~/.cargo/registry 79 + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} 80 + - name: Cache cargo build 81 + uses: actions/cache@v1 82 + with: 83 + path: target 84 + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('.github/workflows/cache_version') }} 85 + - name: Rust toolchain fetch 86 + uses: actions-rs/toolchain@v1 87 + with: 88 + profile: minimal 89 + toolchain: nightly 90 + override: true 91 + components: rustfmt, clippy 92 + - name: Formatting check 93 + continue-on-error: true 94 + uses: actions-rs/cargo@v1 95 + with: 96 + command: fmt 97 + args: --all -- --check 98 + - name: Build Release 99 + run: cargo build --release 100 + - name: Copy executable (Windows) 101 + run: | 102 + mkdir ci-output-release 103 + copy target\release\intiface-engine.exe ci-output-release\intiface-engine.exe 104 + - name: Upload artifacts (release) 105 + uses: actions/upload-artifact@v4 106 + with: 107 + name: intiface-engine-${{ runner.os }}-unstable-v4-release 108 + path: ci-output-release 109 + release: 110 + name: Release artifacts 111 + needs: 112 + - build-stable 113 + - build-v4 114 + runs-on: ubuntu-latest 115 + if: startsWith(github.ref, 'refs/tags/v') 116 + steps: 117 + - uses: actions/checkout@v2 118 + - name: Download Artifact (Linux) 119 + uses: actions/download-artifact@v4 120 + with: 121 + name: intiface-engine-Linux-release 122 + - name: Download Artifact (Windows) 123 + uses: actions/download-artifact@v4 124 + with: 125 + name: intiface-engine-Windows-release 126 + - name: Download Artifact (Windows) (v4 Unstable) 127 + uses: actions/download-artifact@v4 128 + with: 129 + name: intiface-engine-Windows-unstable-v4-release 130 + - name: Download Artifact (MacOS) 131 + uses: actions/download-artifact@v4 132 + with: 133 + name: intiface-engine-macOS-release 134 + - name: Zip executables 135 + # This follows the naming convention from C# and JS. Use -j to junk the 136 + # directory structure. 137 + run: | 138 + zip -j intiface-engine-linux-x64-Release.zip intiface-engine-Linux-release/intiface-engine README.md CHANGELOG.md 139 + zip -j intiface-engine-win-x64-Release.zip intiface-engine-Windows-release/intiface-engine.exe README.md CHANGELOG.md 140 + zip -j intiface-engine-win-x64-unstable-v4-Release.zip intiface-engine-Windows-unstable-v4-release/intiface-engine.exe README.md CHANGELOG.md 141 + zip -j intiface-engine-macos-x64-Release.zip intiface-engine-macOS-release/intiface-engine README.md CHANGELOG.md Info.plist 142 + - name: Release 143 + uses: softprops/action-gh-release@v1 144 + if: startsWith(github.ref, 'refs/tags/') 145 + with: 146 + files: | 147 + intiface-engine-linux-x64-Release.zip 148 + intiface-engine-win-x64-Release.zip 149 + intiface-engine-win-x64-unstable-v4-Release.zip 150 + intiface-engine-macos-x64-Release.zip 151 + README.md 152 + CHANGELOG.md 153 + env: 154 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4
crates/intiface_engine/.gitignore
··· 1 + /target 2 + 3 + # IDE specific files 4 + /.idea
+766
crates/intiface_engine/CHANGELOG.md
··· 1 + # Intiface Engine v3.0.8 (2025/04/20) 2 + 3 + - Update to Buttplug v9.0.8 4 + - Lots of new device support 5 + - Bug fixes for serial ports, device limits 6 + 7 + # Intiface Engine v3.0.7 (2024/12/23) 8 + 9 + ## Features 10 + 11 + - Update to Buttplug v9.0.7 12 + - Lots of new device support 13 + - Lovense devices with changed names now still connect 14 + 15 + # Intiface Engine v3.0.6 (2024/12/23) 16 + 17 + ## Features 18 + 19 + - Update to Buttplug v9.0.6 20 + - Lovense Gush 2/Osci 3 device support 21 + 22 + # Intiface Engine v3.0.5 (2024/12/21) 23 + 24 + ## Features 25 + 26 + - Update to Buttplug v9.0.5 27 + - Many devices additions 28 + 29 + # Intiface Engine v3.0.4 (2024/10/06) 30 + 31 + ## Features 32 + 33 + - Update to Buttplug v9.0.4 34 + - Lovense Solace Pro linear movement support 35 + - Lovense Solace (non-pro) fixes 36 + - Some device additions 37 + 38 + # Intiface Engine v3.0.3 (2024/09/29) 39 + 40 + ## Features 41 + 42 + - Update to Buttplug v9.0.2 43 + - Lots of device additions, look at the Buttplug changelog for more info 44 + 45 + # Intiface Engine v3.0.2 (2024/09/02) 46 + 47 + ## Bugfixes 48 + 49 + - Update to Buttplug v9.0.1 50 + - Fixes bug with messages IDs sometimes not getting set 51 + 52 + # Intiface Engine v3.0.1 (2024/09/01) 53 + 54 + ## Features 55 + 56 + - Update to Buttplug v9.0.0 57 + - Starting the Message Spec v4 development line 58 + - There is now a "allow-unstable-v4-connections" feature that will allow for testing throughout 59 + v4 development. Will be removed when Buttplug v10/Message Spec v4 is released. 60 + - Lots of device support for like 10 different brands. It's been more than 3 months! 61 + - Rebuild server backdoor system to just be another Buttplug Server instead of exposing Device 62 + Manager 63 + - The thing I said I'd never do! 64 + 65 + ## Bugfixes 66 + 67 + - Automatically prepend ws:// to repeater addresses if they don't have it already. 68 + 69 + # Intiface Engine v3.0.0 (2024/05/12) 70 + 71 + ## Breaking Changes 72 + 73 + - Device Config File Compatibility 74 + - This update moves to Buttplug v8, which changes our config file capabilities and adds extra API 75 + calls for config file updates. 76 + 77 + ## Features 78 + 79 + - Update to Buttplug v8.0.0 80 + - Rewrite of the device config system 81 + - Lots of device support for JoyHub, Svakom, LoveDistance, etc... 82 + - Some backward compat bugfixes 83 + 84 + # Intiface Engine v2.0.4 (2024/04/20) 85 + 86 + ## Features 87 + 88 + - Update to Buttplug v7.1.16 89 + - Lots of device support for JoyHub, Kiiroo, Lioness 90 + - Fix Lovense Solace issues 91 + 92 + # Intiface Engine v2.0.3 (2024/03/17) 93 + 94 + ## Features 95 + 96 + - Update to Buttplug v7.1.15 97 + - Fix panics that can happen on shutdown in lovense dongle 98 + 99 + # Intiface Engine v2.0.2 (2024/03/16) 100 + 101 + ## Features 102 + 103 + - Update to Buttplug v7.1.14 104 + - Added more device support (see Buttplug CHANGELOG) 105 + 106 + # Intiface Engine v2.0.1 (2024/01/27) 107 + 108 + ## Features 109 + 110 + - Update to Buttplug v7.1.13 111 + - Added more device support (see Buttplug CHANGELOG) 112 + 113 + # Intiface Engine v2.0.0 (2024/01/21) 114 + 115 + ## Breaking Changes 116 + 117 + - Removed sentry/crash reporting 118 + - This is now a library AND a CLI. If someone is using the CLI, they're using it in their own 119 + setup they can wrap it in whatever crash reporter they want. Moving crash reporting up to Intiface Central. 120 + - Removed logging for library instances 121 + - Intiface was originally built as a CLI and meant to be run only as such. Now that it's a CLI and 122 + a library, we need to let applications handle their own logging. The CLI build still has logging features, but library now just exposes a log/tracing interface. 123 + - Removed Websocket Frontend 124 + - This was used when we were letting other programs run the CLI. Now that there's a library mode, 125 + we expect applications to just attach directly. This makes things more secure overall, and if users want it back, they can implement their own frontend using the trait. 126 + - All above changes will mostly be reflected externally in either missing CLI arguments, or updates 127 + to the EngineOptions struct. 128 + - The v2 line may be fairly short, as the engine will once again have a major revision once Buttplug 129 + moves to its new spec and therefore new major revision. 130 + 131 + ## Features 132 + 133 + - Update to Buttplug v7.1.12 134 + - Massive number of hardware support updates/bugfixes, just go look at the CHANGELOG 135 + - Fixes bugs with streaming JSON 136 + - Moved to tokio-tungstenite 137 + - Matched move made by Buttplug 138 + - Implemented repeater mode (Basic websocket proxy) 139 + - Mostly needed for reflecting desktop browser apps to phone control 140 + 141 + # Intiface Engine v1.4.10 (2023/11/18) 142 + 143 + ## Bugfixes 144 + 145 + - Update to Buttplug v7.1.11 146 + - Fixed btleplug compilation issue on macOS 147 + 148 + # Intiface Engine v1.4.9 (2023/11/18) 149 + 150 + ## Features 151 + 152 + - Update to Buttplug v7.1.10 153 + - Fixes issues with invalid bluetooth names on Android 154 + 155 + # Intiface Engine v1.4.8 (2023/11/16) 156 + 157 + ## Features 158 + 159 + - Update to Buttplug v7.1.9 160 + - Added Lovense Solace, OhMiBod Foxy, Chill support 161 + 162 + # Intiface Engine v1.4.7 (2023/11/04) 163 + 164 + ## Features 165 + 166 + - Allow logging to use environment variables for setup over command line prefs 167 + - Update to Buttplug v7.1.8 168 + - Add lovense device support 169 + - Fix some device support issues 170 + 171 + # Intiface Engine v1.4.6 (2023/10/19) 172 + 173 + ## Features 174 + 175 + - Update to Buttplug v7.1.7 176 + - Fixes memory leak in mDNS handling 177 + - Defaults to device keepalive being on when compiling for iOS 178 + 179 + # Intiface Engine v1.4.5 (2023/10/08) 180 + 181 + ## Features 182 + 183 + - Update to Buttplug v7.1.6 184 + - Fixes Lovense Dongle support 185 + - Added Foreo device support 186 + 187 + # Intiface Engine v1.4.4 (2023/10/05) 188 + 189 + ## Bugfixes 190 + 191 + - Make mDNS actually work in all cases (but it's still considered experimental) 192 + - Fix compilation issues for android 193 + 194 + # Intiface Engine v1.4.3 (2023/10/04) 195 + 196 + ## Features 197 + 198 + - Update to Buttplug v7.1.5 199 + - Lots of device additions, HID device manager for Joycons 200 + - Add mDNS broadcast capabilities 201 + 202 + # Intiface Engine v1.4.2 (2023/07/16) 203 + 204 + ## Features 205 + 206 + - Update to Buttplug v7.1.2 207 + - Device additions for Magic Motion, Lovense Connect bugfix 208 + 209 + # Intiface Engine v1.4.1 (2023/07/09) 210 + 211 + ## Features 212 + 213 + - Update to Buttplug v7.1.1 214 + - Mostly device additions/updates 215 + 216 + # Intiface Engine v1.4.0 (2023/05/21) 217 + 218 + ## Features 219 + 220 + - Update to Buttplug v7.1.0 221 + - Mostly device additions/updates 222 + - Some fixes for user configs 223 + - Move ButtplugRemoteServer into Intiface Engine 224 + - Gives us more flexibility to change things in development 225 + - Updates for user device config updates via Buttplug 226 + 227 + # Intiface Engine v1.3.0 (2023/02/19) 228 + 229 + ## Features 230 + 231 + - Added Websocket Client argument for running the engine as a websocket client instead of a server 232 + - Update to Buttplug v7.0.2 233 + - Hardware protocols updates for Kizuna/Svakom/Sakuraneko 234 + 235 + # Intiface Engine v1.2.2 (2023/01/30) 236 + 237 + ## Bugfixes 238 + 239 + - Fix timing issue on sending EngineStopped message on exit 240 + 241 + # Intiface Engine v1.2.1 (2023/01/16) 242 + 243 + ## Features 244 + 245 + - Update to Buttplug v7.0.1 246 + - Hardware protocol updates/fixed, see Buttplug CHANGELOG for more info. 247 + 248 + # Intiface Engine v1.2.0 (2023/01/01) 249 + 250 + ## Features 251 + 252 + - Update to Buttplug v7.0.0 253 + - Major version move because of API breakage. 254 + - Mostly bugfixes otherwise. 255 + - Removes IPC Pipes, so removed them in Intiface Engine too. 256 + 257 + # Intiface Engine v1.1.0 (2022/12/19) 258 + 259 + ## Features 260 + 261 + - Update to Buttplug v6.3.0 262 + - Lots of device additions 263 + - Major bugfixes for WeVibe/Satisfyer/Magic Motion and Lovense Connect 264 + 265 + # Intiface Engine v1.0.5 (2022/11/27) 266 + 267 + ## Bugfixes 268 + 269 + - Update to Buttplug v6.2.2 270 + - Fixes issues with platform dependencies and DCMs 271 + - Fixes error message in common path in CoreBluetooth 272 + - Stops devices when server disconnects 273 + 274 + # Intiface Engine v1.0.4 (2022/11/24) 275 + 276 + ## Features 277 + 278 + - Update to Buttplug v6.2.1 279 + - Add optional tokio_console feature for task debugging 280 + - Remove crash reporting for now 281 + - Needs to be updated, more testing, etc... 282 + 283 + # Intiface Engine v1.0.3 (2022/11/05) 284 + 285 + ## Features 286 + 287 + - Implemented BackdoorServer, which allows access to server devices directly, while still allowing a 288 + client to access them simultaneously. Can't possibly see how this could go wrong. 289 + - Added EngineServerCreated Event for IntifaceCentral to know when to bring up the BackdoorServer. 290 + 291 + ## Bugfixes 292 + 293 + - Fixed issue where logging could stay alive through multiple server bringups when run in process. 294 + 295 + # Intiface Engine v1.0.2 (2022/10/18) 296 + 297 + ## Bugfixes 298 + 299 + - Vergen should not block building as a library dependency 300 + 301 + # Intiface Engine v1.0.1 (2022/10/15) 302 + 303 + ## Features 304 + 305 + - Update to Buttplug v6.1.0 306 + - Mostly bugfixes 307 + - Now requires v2.x device config files 308 + 309 + # Intiface Engine v1.0.0 (2022/10/01) 310 + 311 + ## Breaking Changes 312 + 313 + - Rebuilt command line arguments 314 + - Now in kebab case format 315 + - ALL DCMs require --use statements, there are no default DCMs anymore 316 + - Incorporates changes made during the egui betas. 317 + - The `--stay_open` argument is now assumed. The server will run until either Ctrl-C is pressed or 318 + an IPC stop message is received. 319 + 320 + ## Features 321 + 322 + - Intiface Engine is now compiled as both a CLI (for desktop) and a Library (for mobile). 323 + - Updated to Buttplug v6 324 + - Moved to semantic versioning, major version denotes CLI argument or breaking IPC protocol change. 325 + 326 + # v101 (egui Beta 2) (2021/01/25) 327 + 328 + - Add websocket device server port selection 329 + 330 + # v100 (egui Beta 1) (2021/01/04) 331 + 332 + ## Features 333 + 334 + - Use JSON over named pipes instead of protobufs over stdio 335 + - Add sentry crash logging 336 + - Server version now uses a shorter tag 337 + - Update to Rust 2021 338 + 339 + # v50 (2022/04/26) - Last version of Intiface CLI 340 + 341 + ## Features 342 + 343 + - Update to Buttplug v5.1.9 344 + - Add Magic Motion Crystal support 345 + - Fix issues with Satisfyer Plugalicious 2 connections 346 + - Fix issues with Satisfyer device identification 347 + 348 + # v49 (2022/03/05) 349 + 350 + ## Features 351 + 352 + - Update to Buttplug v5.1.8 353 + - Added Lelo F1s v2 support, more support for Mannuo/Magic Motion/OhMiBod devices 354 + - May fix issues with windows bluetooth on older Win 10 versions 355 + 356 + # v48 (2021/01/24) 357 + 358 + ## Features 359 + 360 + - Update to Buttplug v5.1.7 361 + - Lovense Calor support, Folove support, more WeVibe/Satisfyer support 362 + 363 + # v47 (2022/01/04) 364 + 365 + ## Bugfixes 366 + 367 + - No changes to build, re-release to fix issue with a wrong tag getting pushed. 368 + 369 + # v46 (2022/01/01) 370 + 371 + ## Bugfixes 372 + 373 + - Update to Buttplug v5.1.6 374 + - Fix issues with serial ports blocking, lovense connect data types, log message levels, etc... 375 + - See Buttplug v5.1.6 changelog for more info. 376 + (https://github.com/buttplugio/buttplug/blob/master/buttplug/CHANGELOG.md) 377 + 378 + # v45 (2021/12/19) 379 + 380 + ## Bugfixes 381 + 382 + - Update to Buttplug v5.1.5 383 + - Fix issues with Satisfyer name detection and disconnection 384 + - Fix issues with device scanning always saying it's instantly finished 385 + 386 + # v44 (2021/12/14) 387 + 388 + ## Bugfixes 389 + 390 + - Update to Buttplug v5.1.4 391 + - Shouldn't change anything in here, all the fixes were FFI related, but eh. 392 + - Try to get crash logs into frontend log output for easier debugging 393 + - #14: Fix issue with intiface-cli not sending events to desktop after first disconnection 394 + 395 + # v43 (2021/12/04) 396 + 397 + ## Bugfixes 398 + 399 + - Update to Buttplug v5.1.2 400 + - Fix race condition with bluetooth advertisements causing multiple simultaneous connects to 401 + devices 402 + - Update to vergen 5.2.0 403 + - Last version was yanked 404 + 405 + # v42 (2021/12/03) 406 + 407 + ## Bugfixes 408 + 409 + - Update to Buttplug v5.1.1 410 + - Fix issues with devices w/ advertised services being ignored 411 + - Fix issues with lovense dongle on linux 412 + 413 + # v41 (2021/12/02) 414 + 415 + ## Features 416 + 417 + - Update to Buttplug v5.1 418 + - Bluetooth library updates 419 + - Satisfyer/ManNuo/other device support (see Buttplug README) 420 + - Lots of other fixes 421 + - Update to vergen v5, tracing-subscriber v0.3 422 + 423 + # v40 (2021/09/14) 424 + 425 + ## Features 426 + 427 + - Update to Buttplug v5.0.1 428 + - Better MacOS bluetooth support 429 + - Better Linux bluetooth support 430 + - Tons of device additions (see Buttplug README) 431 + - Adds websocket device interface 432 + 433 + # v39 (2021/07/05) 434 + 435 + ## Features 436 + 437 + - Server now throws warnings whenever a client tries to connect when another client is already 438 + connected. 439 + - Update to Buttplug 4.0.4 440 + - Added hardware support for TCode devices, Patoo, Vorze Piston SA 441 + 442 + ## Bugfixes 443 + 444 + - Fix cancellation of tasks on shutdown. 445 + 446 + # v38 (2021/06/18) 447 + 448 + ## Bugfixes 449 + 450 + - Update to buttplug-rs 4.0.3, which fixes issues with Android phones using the Lovense Connect app. 451 + 452 + # v37 (2021/06/11) 453 + 454 + ## Bugfixes 455 + 456 + - Fix timing issue where Process Ended message may not be seen by Intiface Desktop 457 + - Update to buttplug-rs 4.0.2, fixing issue with Intiface Desktop stalling due to logging issues. 458 + - Add Info.plist file for macOS Big Sur and later compat 459 + 460 + # v36 (2021/06/10) 461 + 462 + ## Features 463 + 464 + - Added opt-in/out arguments for all available device communication managers 465 + - Added support for Lovense Connect Service 466 + 467 + # v35 (2021/04/04) 468 + 469 + ## Bugfixes 470 + 471 + - Update to Buttplug v2.1.9 472 + - Reduces error log messages thrown by lovense dongle 473 + - Reduces panics in bluetooth handling on windows 474 + - Fixes issue with battery checking on lovense devices stalling library on device disconnect 475 + 476 + # v34 (2021/03/25) 477 + 478 + ## Bugfixes 479 + 480 + - Update to Buttplug v2.1.8 481 + - Possibly fixes issue with bluetooth devices not registering disconnection on windows. 482 + 483 + # v33 (2021/03/08) 484 + 485 + ## Bugfixes 486 + 487 + - Update to Buttplug v2.1.7 488 + - Fixes legacy message issues with The Handy and Vorze toys 489 + - Fixes init issues with some Kiiroo vibrators 490 + 491 + # v32 (2021/02/28) 492 + 493 + ## Bugfixes 494 + 495 + - Update to Buttplug v2.1.6 496 + - Fixes issues with log message spamming 497 + - Update btleplug to 0.7.0, lots of cleanup 498 + 499 + # v31 (2021/02/20) 500 + 501 + ## Bugfixes 502 + 503 + - Update to Buttplug v2.1.5 504 + - Fixes panic in devices that disconnect during initialize(). 505 + 506 + # v30 (2021/02/13) 507 + 508 + ## Features 509 + 510 + - Update to Buttplug v2.1.4 511 + - Added Hardware Support 512 + - The Handy 513 + 514 + ## Bugfixes 515 + 516 + - Fixes issues with the LoveAi Dolp and Lovense Serial Dongle 517 + 518 + # v29 (2021/02/06) 519 + 520 + ## Bugfixes 521 + 522 + - Update to Buttplug v2.1.3 523 + - Fix StopAllDevices so it actually stops all devices again 524 + - Allow for setting device intensity to 1.0 525 + 526 + # v28 (2021/02/06) 527 + 528 + ## Features 529 + 530 + - Update to Buttplug v2.1.1 531 + - Adds Lovense Diamo and Nobra's Silicone Dreams support 532 + - Lots of bugfixes and more/better errors being emitted 533 + 534 + # v27 (2021/01/24) 535 + 536 + ## Bugfixes 537 + 538 + - Update to Buttplug 2.0.5 539 + - Fixes issue with v2 protocol conflicts in DeviceMessageInfo 540 + 541 + # v26 (2021/01/24) 542 + 543 + ## Bugfixes 544 + 545 + - Update to Buttplug 2.0.4 546 + - Fixes issue with XInput devices being misaddressed and stopping all scanning. 547 + 548 + # v25 (2021/01/19) 549 + 550 + ## Bugfixes 551 + 552 + - Update to Buttplug 2.0.2 553 + - Fixes issue with scanning status getting stuck on Lovense dongles 554 + 555 + # v24 (Yanked) (2021/01/18) 556 + 557 + ## Features 558 + 559 + - Update to Buttplug 2.0.1 560 + - Event system and API cleanup 561 + - Lovense Ferri Support 562 + - Backtraces now emitted via logging system when using frontend IPC 563 + 564 + # v23 (2021/01/01) 565 + 566 + ## Bugfixes 567 + 568 + - Update to Buttplug 1.0.4 569 + - Fixes issues with XInput Gamepads causing intiface-cli-rs crashes on reconnect. 570 + 571 + # v22 (2021/01/01) 572 + 573 + ## Bugfixes 574 + 575 + - Update to Buttplug 1.0.3 576 + - Fixes issues with BTLE advertisements and adds XInput device rescanning. 577 + 578 + # v21 (2020/12/31) 579 + 580 + ## Bugfixes 581 + 582 + - Update to Buttplug 1.0.1 583 + - Fixes issue with device scanning races. 584 + 585 + # v20 (2020/12/22) 586 + 587 + ## Bugfixes 588 + 589 + - Update to Buttplug 0.11.3 590 + - Fixes security issues and a memory leak when scanning is called often. 591 + 592 + # v19 (2020/12/11) 593 + 594 + ## Bugfixes 595 + 596 + - Update to Buttplug 0.11.2 597 + - Emits Scanningfinished when scanning is finished. Finally. 598 + 599 + # v18 (2020/11/27) 600 + 601 + ## Features 602 + 603 + - Update to buttplug-rs 0.11.1 604 + - System bugfixes 605 + - Mysteryvibe support 606 + 607 + # v17 (2020/10/25) 608 + 609 + ## Features 610 + 611 + - Update to buttplug-rs 0.10.1 612 + - Lovense Dongle Bugfixes 613 + - BLE Toy Connection Bugfixes 614 + - Fix logging output 615 + - Pay attention to log option on command line again 616 + - Outputs full tracing JSON to frontend 617 + 618 + # v16 (2020/10/17) 619 + 620 + ## Features 621 + 622 + - Update to buttplug-rs 0.10.0 623 + - Kiiroo Keon Support 624 + - New raw device commands (use --allowraw option for access) 625 + 626 + ## Bugfixes 627 + 628 + - Update to buttplug-rs 0.10.0 629 + - Lots of websocket crash fixes 630 + 631 + # v15 (2020/10/05) 632 + 633 + ## Bugfixes 634 + 635 + - Update to buttplug-rs 0.9.2 w/ btleplug 0.5.4, fixing an issue with macOS 636 + panicing whenever it tries to read from a BLE device. 637 + 638 + # v14 (2020/10/05) 639 + 640 + ## Bugfixes 641 + 642 + - Update to buttplug-rs 0.9.1 w/ btleplug 0.5.3, fixing an issue with macOS 643 + panicing whenever it tries to write to a BLE device. 644 + 645 + # v13 (2020/10/04) 646 + 647 + ## Features 648 + 649 + - Update to buttplug-rs 0.9.0, which now has Battery level reading capabilites 650 + for some hardware. 651 + 652 + ## Bugfixes 653 + 654 + - Update to buttplug-rs 0.9.0, which now does not crash when 2 devices are 655 + connected and one disconnects. 656 + 657 + # v12 (2020/10/02) 658 + 659 + ## Features 660 + 661 + - Update to Buttplug-rs 0.8.4, fixing a bunch of device issues. 662 + - Default to outputting info level logs if no env log var set. (Should pick this 663 + up from command line argument in future version) 664 + 665 + ## Bugfixes 666 + 667 + - Only run for one connection attempt if --stayopen isn't passed in. 668 + 669 + # v11 (2020/09/20) 670 + 671 + ## Bugfixes 672 + 673 + - Moves to buttplug-0.8.3, which fixes support for some programs using older 674 + APIs (FleshlightLaunchFW12Cmd) for Kiiroo stroking products (Onyx, Fleshlight 675 + Launch, etc). 676 + 677 + # v10 (2020/09/13) 678 + 679 + ## Features 680 + 681 + - Added log handling from Buttplug library. Still needs protocol/CLI setting, 682 + currently outputs everything INFO or higher. 683 + 684 + ## Bugfixes 685 + 686 + - Moves to buttplug-0.8.2, fixing Lovense rotation and adding log output 687 + support. 688 + 689 + # v9 (2020/09/11) 690 + 691 + ## Bugfixes 692 + 693 + - Moves to buttplug-0.7.3, which loads both RSA and pkcs8 certificates. This 694 + allows us to load the certs that come from Intiface Desktop. 695 + 696 + # v8 (2020/09/07) 697 + 698 + ## Bugfixes 699 + 700 + - Move to buttplug-rs 0.7.2, which adds more device configurations and fixes 701 + websocket listening on all interfaces. 702 + 703 + # v7 (2020/09/06) 704 + 705 + ## Features 706 + 707 + - Move to buttplug-rs 0.7.1, which includes status emitting features and way 708 + more device protocol support. 709 + - Allow frontend to trigger process stop 710 + - Send disconnect to frontend when client disconnects 711 + - Can now relay connected/disconnected devices to GUIs via PBuf protocol 712 + 713 + # v6 (2020/08/06) 714 + 715 + ## Features 716 + 717 + - Move to buttplug-rs 0.6.0, which integrates websockets and server lifetime 718 + handling. intiface-cli-rs is now a very thin wrapper around buttplug-rs, 719 + handling system bringup and frontend communication and that's about it. 720 + 721 + # v5 (2020/05/13) 722 + 723 + ## Bugfixes 724 + 725 + - Move to buttplug-rs 0.3.1, with a couple of unwrap fixes 726 + 727 + # v4 (2020/05/10) 728 + 729 + ## Features 730 + 731 + - --stayopen option now actually works, reusing the server between 732 + client connections. 733 + 734 + # v3 (2020/05/09) 735 + 736 + ## Features 737 + 738 + - Added protobuf basis for hooking CLI into Intiface Desktop 739 + 740 + ## Bugfixes 741 + 742 + - Fixed bug where receiving ping message from async_tungstenite would 743 + panic server 744 + - Update to buttplug 0.2.4, which fixes ServerInfo message ID matching 745 + 746 + # v2 (2020/02/15) 747 + 748 + ## Features 749 + 750 + - Move to using rolling versioning, since this is a binary 751 + - Move to using buttplug 0.2, with full server implementation 752 + - Add cert generation 753 + - Add secure websocket capabilities 754 + - Move to using async-tungstenite 755 + - Use Buttplug's built in JSONWrapper 756 + - Add XInput capability on windows 757 + - Add CI building 758 + - Add Simple GUI message output for Intiface Desktop 759 + 760 + # v1 (aka v0.0.1) (2020/02/15) 761 + 762 + ## Features 763 + 764 + - First version 765 + - Can bring up insecure websocket, run server, access toys 766 + - Most options not used yet
+68
crates/intiface_engine/Cargo.toml
··· 1 + [package] 2 + name = "intiface-engine" 3 + version = "3.0.8" 4 + authors = ["Nonpolynomial Labs, LLC <kyle@nonpolynomial.com>"] 5 + description = "CLI and Library frontend for the Buttplug sex toy control library" 6 + license = "BSD-3-Clause" 7 + homepage = "http://intiface.com" 8 + repository = "https://github.com/intiface/intiface-engine.git" 9 + readme = "README.md" 10 + keywords = ["usb", "serial", "hardware", "bluetooth", "teledildonics"] 11 + edition = "2021" 12 + exclude = [".vscode/**"] 13 + 14 + [lib] 15 + name = "intiface_engine" 16 + path = "src/lib.rs" 17 + 18 + [[bin]] 19 + name = "intiface-engine" 20 + path = "src/bin/main.rs" 21 + 22 + [features] 23 + default=[] 24 + tokio-console=["console-subscriber"] 25 + 26 + [dependencies] 27 + buttplug_core = { path = "../buttplug_core" } 28 + buttplug_server = { path = "../buttplug_server" } 29 + buttplug_server_device_config = { path = "../buttplug_server_device_config" } 30 + buttplug_server_hwmgr_btleplug = { path = "../buttplug_server_hwmgr_btleplug" } 31 + buttplug_server_hwmgr_hid = { path = "../buttplug_server_hwmgr_hid" } 32 + buttplug_server_hwmgr_lovense_connect = { path = "../buttplug_server_hwmgr_lovense_connect" } 33 + buttplug_server_hwmgr_lovense_dongle = { path = "../buttplug_server_hwmgr_lovense_dongle" } 34 + buttplug_server_hwmgr_serial = { path = "../buttplug_server_hwmgr_serial" } 35 + buttplug_server_hwmgr_websocket = { path = "../buttplug_server_hwmgr_websocket" } 36 + buttplug_server_hwmgr_xinput = { path = "../buttplug_server_hwmgr_xinput" } 37 + buttplug_transport_websocket_tungstenite = { path = "../buttplug_transport_websocket_tungstenite" } 38 + # buttplug = "9.0.8" 39 + argh = "0.1.13" 40 + log = "0.4.27" 41 + futures = "0.3.31" 42 + tracing-fmt = "0.1.1" 43 + tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } 44 + tracing = "0.1.41" 45 + tokio = { version = "1.45.0", features = ["sync", "rt-multi-thread", "macros", "io-std", "fs", "signal", "io-util"] } 46 + log-panics = { version = "2.1.0", features = ["with-backtrace"] } 47 + backtrace = "0.3.75" 48 + ctrlc = "3.4.7" 49 + tokio-util = "0.7.15" 50 + serde = "1.0.219" 51 + serde_json = "1.0.140" 52 + thiserror = "2.0.12" 53 + getset = "0.1.5" 54 + async-trait = "0.1.88" 55 + once_cell = "1.21.3" 56 + lazy_static = "1.5.0" 57 + console-subscriber = { version="0.4.1", optional = true } 58 + local-ip-address = "0.6.5" 59 + rand = "0.9.1" 60 + tokio-tungstenite = "0.26.2" 61 + futures-util = "0.3.31" 62 + url = "2.5.4" 63 + libmdns = "0.9.1" 64 + tokio-stream = "0.1.17" 65 + 66 + [build-dependencies] 67 + vergen-gitcl = {version = "1.0.8", features = ["build"]} 68 + anyhow = "1.0.98"
+105
crates/intiface_engine/README.md
··· 1 + # Intiface Engine 2 + 3 + [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) 4 + [![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) 5 + [![Discourse Forums](https://img.shields.io/discourse/status?label=buttplug.io%20forums&server=https%3A%2F%2Fdiscuss.buttplug.io)](https://discuss.buttplug.io) 6 + [![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.buttplug.io) 7 + [![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) 8 + 9 + ![Intiface Engine Build](https://github.com/intiface/intiface-engine/workflows/Intiface%20Engine%20Build/badge.svg) ![crates.io](https://img.shields.io/crates/v/intiface-engine.svg) 10 + 11 + 12 + <p align="center"> 13 + <img src="https://raw.githubusercontent.com/buttplugio/buttplug/dev/images/buttplug_rust_docs.png"> 14 + </p> 15 + 16 + CLI and Library frontend for Buttplug 17 + 18 + Intiface Engine is just a front-end for [Buttplug](https://github.com/buttplugio/buttplug), 19 + but since we're trying to not make people install a program named "Buttplug", here we are. 20 + 21 + While this program can be used standalone, it will mostly be featured as a backend/engine for 22 + Intiface Central. 23 + 24 + ## Running 25 + 26 + Command line options are as follows: 27 + 28 + | Option | Description | 29 + | --------- | --------- | 30 + | `version` | Print version and exit | 31 + | `server-version` | Print version and exit (kept for legacy reasons) | 32 + | `websocket-use-all-interfaces` | Websocket servers will listen on all interfaces (versus only on localhost, which is default) | 33 + | `websocket-port [port]` | Network port for connecting via non-ssl (ws://) protocols | 34 + | `frontend-websocket-port` | IPC JSON port for Intiface Central | 35 + | `server-name` | Identifying name server should emit when asked for info | 36 + | `device-config-file [file]` | Device configuration file to load (if omitted, uses internal) | 37 + | `user-device-config-file [file]` | User device configuration file to load (if omitted, none used) | 38 + | `max-ping-time [number]` | Milliseconds for ping time limit of server (if omitted, set to 0) | 39 + | `log` | Level of logs to output by default (if omitted, set to None) | 40 + | `allow-raw` | Allow clients to communicate using raw messages (DANGEROUS, CAN BRICK SOME DEVICES) | 41 + | `use-bluetooth-le` | Use the Bluetooth LE Buttplug Device Communication Manager | 42 + | `use-serial` | Use the Serial Port Buttplug Device Communication Manager | 43 + | `use-hid` | Use the HID Buttplug Device Communication Manager | 44 + | `use-lovense-dongle` | Use the HID Lovense Dongle Buttplug Device Communication Manager | 45 + | `use-xinput` | Use the XInput Buttplug Device Communication Manager | 46 + | `use-lovense-connect` | Use the Lovense Connect Buttplug Device Communication Manager | 47 + | `use-device-websocket-server` | Use the Device Websocket Server Buttplug Device Communication Manager | 48 + | `device-websocket-server-port` | Port for the device websocket server | 49 + 50 + For example, to run the server on websockets at port 12345 with bluetooth device support: 51 + 52 + `intiface-engine --websocket-port 12345 --use-bluetooth-le` 53 + 54 + ## Compiling 55 + 56 + Linux will have extra compilation dependency requirements via 57 + [buttplug-rs](https://github.com/buttplugio/buttplug-rs). For pacakges required, 58 + please check there. 59 + 60 + ## Contributing 61 + 62 + Right now, we mostly need code/API style reviews and feedback. We don't really have any good 63 + bite-sized chunks to mentor the implementation yet, but one we do, those will be marked "Help 64 + Wanted" in our [github issues](https://github.com/buttplugio/buttplug-rs/issues). 65 + 66 + As we need money to keep up with supporting the latest and greatest hardware, we also have multiple 67 + ways to donate! 68 + 69 + - [Patreon](https://patreon.com/qdot) 70 + - [Github Sponsors](https://github.com/sponsors/qdot) 71 + - [Ko-Fi](https://ko-fi.com/qdot76367) 72 + 73 + ## License and Trademarks 74 + 75 + Intiface is a Registered Trademark of Nonpolynomial Labs, LLC 76 + 77 + Buttplug and Intiface are BSD licensed. 78 + 79 + Copyright (c) 2016-2022, Nonpolynomial Labs, LLC 80 + All rights reserved. 81 + 82 + Redistribution and use in source and binary forms, with or without 83 + modification, are permitted provided that the following conditions are met: 84 + 85 + * Redistributions of source code must retain the above copyright notice, this 86 + list of conditions and the following disclaimer. 87 + 88 + * Redistributions in binary form must reproduce the above copyright notice, 89 + this list of conditions and the following disclaimer in the documentation 90 + and/or other materials provided with the distribution. 91 + 92 + * Neither the name of buttplug nor the names of its 93 + contributors may be used to endorse or promote products derived from 94 + this software without specific prior written permission. 95 + 96 + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 97 + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 98 + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 99 + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 100 + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 101 + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 102 + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 103 + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 104 + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 105 + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+14
crates/intiface_engine/build.rs
··· 1 + use anyhow::Result; 2 + use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; 3 + 4 + fn main() -> Result<()> { 5 + let build = BuildBuilder::default().build_timestamp(true).build()?; 6 + let gitcl = GitclBuilder::default().sha(true).build()?; 7 + 8 + Emitter::default() 9 + .add_instructions(&build)? 10 + .add_instructions(&gitcl)? 11 + .emit()?; 12 + 13 + Ok(()) 14 + }
+4
crates/intiface_engine/config.toml
··· 1 + [target.x86_64-pc-windows-msvc] 2 + rustflags = ["-Ctarget-feature=+crt-static"] 3 + [target.i686-pc-windows-msvc] 4 + rustflags = ["-Ctarget-feature=+crt-static"]
+1
crates/intiface_engine/rustfmt.toml
··· 1 + tab_spaces = 2
+75
crates/intiface_engine/src/backdoor_server.rs
··· 1 + use buttplug_core::{ 2 + connector::transport::stream::ButtplugStreamTransport, 3 + message::serializer::ButtplugSerializedMessage, 4 + util::stream::convert_broadcast_receiver_to_stream, 5 + }; 6 + use buttplug_server::{connector::ButtplugRemoteServerConnector, device::ServerDeviceManager, message::serializer::ButtplugServerJSONSerializer, ButtplugServerBuilder}; 7 + use std::sync::Arc; 8 + use tokio::sync::{ 9 + broadcast, 10 + mpsc::{self, Sender}, 11 + }; 12 + use tokio_stream::Stream; 13 + 14 + use crate::ButtplugRemoteServer; 15 + 16 + // Allows direct access to the Device Manager of a running ButtplugServer. Bypasses requirements for 17 + // client handshake, ping, etc... 18 + pub struct BackdoorServer { 19 + //server: ButtplugRemoteServer, 20 + sender: Sender<ButtplugSerializedMessage>, 21 + broadcaster: broadcast::Sender<String>, 22 + } 23 + 24 + impl BackdoorServer { 25 + pub fn new(device_manager: Arc<ServerDeviceManager>) -> Self { 26 + let server = ButtplugRemoteServer::new( 27 + ButtplugServerBuilder::with_shared_device_manager(device_manager.clone()) 28 + .name("Intiface Backdoor Server") 29 + .finish() 30 + .unwrap(), 31 + ); 32 + let (s_out, mut r_out) = mpsc::channel(255); 33 + let (s_in, r_in) = mpsc::channel(255); 34 + let (s_stream, _) = broadcast::channel(255); 35 + tokio::spawn(async move { 36 + if let Err(e) = server 37 + .start(ButtplugRemoteServerConnector::< 38 + _, 39 + ButtplugServerJSONSerializer, 40 + >::new(ButtplugStreamTransport::new(s_out, r_in))) 41 + .await 42 + { 43 + // We can't do much if the server fails, but we *can* yell into the logs! 44 + error!("Backdoor server error: {:?}", e); 45 + } 46 + }); 47 + let sender_clone = s_stream.clone(); 48 + tokio::spawn(async move { 49 + while let Some(ButtplugSerializedMessage::Text(m)) = r_out.recv().await { 50 + if sender_clone.receiver_count() == 0 { 51 + continue; 52 + } 53 + if sender_clone.send(m).is_err() { 54 + break; 55 + } 56 + } 57 + }); 58 + Self { 59 + sender: s_in, 60 + broadcaster: s_stream, 61 + } 62 + } 63 + 64 + pub fn event_stream(&self) -> impl Stream<Item = String> + '_ { 65 + convert_broadcast_receiver_to_stream(self.broadcaster.subscribe()) 66 + } 67 + 68 + pub async fn parse_message(&self, msg: &str) { 69 + self 70 + .sender 71 + .send(ButtplugSerializedMessage::Text(msg.to_owned())) 72 + .await 73 + .unwrap(); 74 + } 75 + }
+309
crates/intiface_engine/src/bin/main.rs
··· 1 + use argh::FromArgs; 2 + use getset::{CopyGetters, Getters}; 3 + use intiface_engine::{ 4 + EngineOptions, EngineOptionsBuilder, IntifaceEngine, IntifaceEngineError, IntifaceError, 5 + }; 6 + use std::fs; 7 + use tokio::{select, signal::ctrl_c}; 8 + use tracing::{debug, info, Level}; 9 + use tracing_subscriber::{ 10 + filter::{EnvFilter, LevelFilter}, 11 + layer::SubscriberExt, 12 + util::SubscriberInitExt, 13 + }; 14 + 15 + const VERSION: &str = env!("CARGO_PKG_VERSION"); 16 + 17 + /// command line interface for intiface/buttplug. 18 + /// 19 + /// Note: Commands are one word to keep compat with C#/JS executables currently. 20 + #[derive(FromArgs, Getters, CopyGetters)] 21 + pub struct IntifaceCLIArguments { 22 + // Options that do something then exit 23 + /// print version and exit. 24 + #[argh(switch)] 25 + #[getset(get_copy = "pub")] 26 + version: bool, 27 + 28 + /// print version and exit. 29 + #[argh(switch)] 30 + #[getset(get_copy = "pub")] 31 + server_version: bool, 32 + 33 + // Options that set up the server networking 34 + /// if passed, websocket server listens on all interfaces. Otherwise, only 35 + /// listen on 127.0.0.1. 36 + #[argh(switch)] 37 + #[getset(get_copy = "pub")] 38 + websocket_use_all_interfaces: bool, 39 + 40 + /// insecure port for websocket servers. 41 + #[argh(option)] 42 + #[getset(get_copy = "pub")] 43 + websocket_port: Option<u16>, 44 + 45 + /// insecure address for connecting to websocket servers. 46 + #[argh(option)] 47 + #[getset(get = "pub")] 48 + websocket_client_address: Option<String>, 49 + 50 + // Options that set up communications with intiface GUI 51 + /// if passed, output json for parent process via websockets 52 + #[argh(option)] 53 + #[getset(get_copy = "pub")] 54 + frontend_websocket_port: Option<u16>, 55 + 56 + // Options that set up Buttplug server parameters 57 + /// name of server to pass to connecting clients. 58 + #[argh(option)] 59 + #[argh(default = "\"Buttplug Server\".to_owned()")] 60 + #[getset(get = "pub")] 61 + server_name: String, 62 + 63 + /// path to the device configuration file 64 + #[argh(option)] 65 + #[getset(get = "pub")] 66 + device_config_file: Option<String>, 67 + 68 + /// path to user device configuration file 69 + #[argh(option)] 70 + #[getset(get = "pub")] 71 + user_device_config_file: Option<String>, 72 + 73 + /// ping timeout maximum for server (in milliseconds) 74 + #[argh(option)] 75 + #[argh(default = "0")] 76 + #[getset(get_copy = "pub")] 77 + max_ping_time: u32, 78 + 79 + /// set log level for output 80 + #[allow(dead_code)] 81 + #[argh(option)] 82 + #[getset(get_copy = "pub")] 83 + log: Option<Level>, 84 + 85 + /// turn off bluetooth le device support 86 + #[argh(switch)] 87 + #[getset(get_copy = "pub")] 88 + use_bluetooth_le: bool, 89 + 90 + /// turn off serial device support 91 + #[argh(switch)] 92 + #[getset(get_copy = "pub")] 93 + use_serial: bool, 94 + 95 + /// turn off hid device support 96 + #[allow(dead_code)] 97 + #[argh(switch)] 98 + #[getset(get_copy = "pub")] 99 + use_hid: bool, 100 + 101 + /// turn off lovense dongle serial device support 102 + #[argh(switch)] 103 + #[getset(get_copy = "pub")] 104 + use_lovense_dongle_serial: bool, 105 + 106 + /// turn off lovense dongle hid device support 107 + #[argh(switch)] 108 + #[getset(get_copy = "pub")] 109 + use_lovense_dongle_hid: bool, 110 + 111 + /// turn off xinput gamepad device support (windows only) 112 + #[argh(switch)] 113 + #[getset(get_copy = "pub")] 114 + use_xinput: bool, 115 + 116 + /// turn on lovense connect app device support (off by default) 117 + #[argh(switch)] 118 + #[getset(get_copy = "pub")] 119 + use_lovense_connect: bool, 120 + 121 + /// turn on websocket server device comm manager 122 + #[argh(switch)] 123 + #[getset(get_copy = "pub")] 124 + use_device_websocket_server: bool, 125 + 126 + /// port for device websocket server comm manager (defaults to 54817) 127 + #[argh(option)] 128 + #[getset(get_copy = "pub")] 129 + device_websocket_server_port: Option<u16>, 130 + 131 + /// if set, broadcast server port/service info via mdns 132 + #[argh(switch)] 133 + #[getset(get_copy = "pub")] 134 + broadcast_server_mdns: bool, 135 + 136 + /// mdns suffix, will be appended to instance names for advertised mdns services (optional, ignored if broadcast_mdns is not set) 137 + #[argh(option)] 138 + #[getset(get = "pub")] 139 + mdns_suffix: Option<String>, 140 + 141 + /// if set, use repeater mode instead of engine mode 142 + #[argh(switch)] 143 + #[getset(get_copy = "pub")] 144 + repeater: bool, 145 + 146 + /// if set, use repeater mode instead of engine mode 147 + #[argh(option)] 148 + #[getset(get_copy = "pub")] 149 + repeater_port: Option<u16>, 150 + 151 + /// if set, use repeater mode instead of engine mode 152 + #[argh(option)] 153 + #[getset(get = "pub")] 154 + repeater_remote_address: Option<String>, 155 + 156 + #[cfg(debug_assertions)] 157 + /// crash the main thread (that holds the runtime) 158 + #[argh(switch)] 159 + #[getset(get_copy = "pub")] 160 + crash_main_thread: bool, 161 + 162 + #[allow(dead_code)] 163 + #[cfg(debug_assertions)] 164 + /// crash the task thread (for testing logging/reporting) 165 + #[argh(switch)] 166 + #[getset(get_copy = "pub")] 167 + crash_task_thread: bool, 168 + } 169 + 170 + pub fn setup_console_logging(log_level: Option<Level>) { 171 + if log_level.is_some() { 172 + tracing_subscriber::registry() 173 + .with(tracing_subscriber::fmt::layer()) 174 + .with(LevelFilter::from(log_level)) 175 + .try_init() 176 + .unwrap(); 177 + } else { 178 + tracing_subscriber::registry() 179 + .with(tracing_subscriber::fmt::layer()) 180 + .with( 181 + EnvFilter::try_from_default_env() 182 + .or_else(|_| EnvFilter::try_new("info")) 183 + .unwrap(), 184 + ) 185 + .try_init() 186 + .unwrap(); 187 + }; 188 + println!("Intiface Server, starting up with stdout output."); 189 + } 190 + 191 + impl TryFrom<IntifaceCLIArguments> for EngineOptions { 192 + type Error = IntifaceError; 193 + fn try_from(args: IntifaceCLIArguments) -> Result<Self, IntifaceError> { 194 + let mut builder = EngineOptionsBuilder::default(); 195 + 196 + if let Some(deviceconfig) = args.device_config_file() { 197 + info!( 198 + "Intiface CLI Options: External Device Config {}", 199 + deviceconfig 200 + ); 201 + match fs::read_to_string(deviceconfig) { 202 + Ok(cfg) => builder.device_config_json(&cfg), 203 + Err(err) => { 204 + return Err(IntifaceError::new(&format!( 205 + "Error opening external device configuration: {:?}", 206 + err 207 + ))) 208 + } 209 + }; 210 + } 211 + 212 + if let Some(userdeviceconfig) = args.user_device_config_file() { 213 + info!( 214 + "Intiface CLI Options: User Device Config {}", 215 + userdeviceconfig 216 + ); 217 + match fs::read_to_string(userdeviceconfig) { 218 + Ok(cfg) => builder.user_device_config_json(&cfg), 219 + Err(err) => { 220 + return Err(IntifaceError::new(&format!( 221 + "Error opening user device configuration: {:?}", 222 + err 223 + ))) 224 + } 225 + }; 226 + } 227 + 228 + builder 229 + .websocket_use_all_interfaces(args.websocket_use_all_interfaces()) 230 + .use_bluetooth_le(args.use_bluetooth_le()) 231 + .use_serial_port(args.use_serial()) 232 + .use_hid(args.use_hid()) 233 + .use_lovense_dongle_serial(args.use_lovense_dongle_serial()) 234 + .use_lovense_dongle_hid(args.use_lovense_dongle_hid()) 235 + .use_xinput(args.use_xinput()) 236 + .use_lovense_connect(args.use_lovense_connect()) 237 + .use_device_websocket_server(args.use_device_websocket_server()) 238 + .max_ping_time(args.max_ping_time()) 239 + .server_name(args.server_name()) 240 + .broadcast_server_mdns(args.broadcast_server_mdns()); 241 + 242 + #[cfg(debug_assertions)] 243 + { 244 + builder 245 + .crash_main_thread(args.crash_main_thread()) 246 + .crash_task_thread(args.crash_task_thread()); 247 + } 248 + 249 + if let Some(value) = args.websocket_port() { 250 + builder.websocket_port(value); 251 + } 252 + if let Some(value) = args.websocket_client_address() { 253 + builder.websocket_client_address(value); 254 + } 255 + if let Some(value) = args.frontend_websocket_port() { 256 + builder.frontend_websocket_port(value); 257 + } 258 + if let Some(value) = args.device_websocket_server_port() { 259 + builder.device_websocket_server_port(value); 260 + } 261 + if args.broadcast_server_mdns() { 262 + if let Some(value) = args.mdns_suffix() { 263 + builder.mdns_suffix(value); 264 + } 265 + } 266 + Ok(builder.finish()) 267 + } 268 + } 269 + 270 + #[tokio::main(flavor = "current_thread")] //#[tokio::main] 271 + async fn main() -> Result<(), IntifaceEngineError> { 272 + let args: IntifaceCLIArguments = argh::from_env(); 273 + if args.server_version() { 274 + println!("{}", VERSION); 275 + return Ok(()); 276 + } 277 + 278 + if args.version() { 279 + debug!("Server version command sent, printing and exiting."); 280 + println!( 281 + "Intiface CLI (Rust Edition) Version {}, Commit {}, Built {}", 282 + VERSION, 283 + option_env!("VERGEN_GIT_SHA_SHORT").unwrap_or("unknown"), 284 + option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown") 285 + ); 286 + return Ok(()); 287 + } 288 + 289 + if args.frontend_websocket_port().is_none() { 290 + setup_console_logging(args.log()); 291 + } 292 + 293 + let options = EngineOptions::try_from(args).map_err(IntifaceEngineError::from)?; 294 + let engine = IntifaceEngine::default(); 295 + select! { 296 + result = engine.run(&options, None, &None) => { 297 + if let Err(e) = result { 298 + println!("Server errored while running:"); 299 + println!("{:?}", e); 300 + } 301 + } 302 + _ = ctrl_c() => { 303 + info!("Control-c hit, exiting."); 304 + engine.stop(); 305 + } 306 + } 307 + 308 + Ok(()) 309 + }
+159
crates/intiface_engine/src/buttplug_server.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::{ 4 + BackdoorServer, ButtplugRemoteServer, ButtplugServerConnectorError, EngineOptions, 5 + IntifaceEngineError, IntifaceError, 6 + }; 7 + use buttplug_transport_websocket_tungstenite::{ 8 + ButtplugWebsocketClientTransport, 9 + ButtplugWebsocketServerTransportBuilder, 10 + }; 11 + use buttplug_server_device_config::{DeviceConfigurationManager, load_protocol_configs}; 12 + use buttplug_server_hwmgr_btleplug::BtlePlugCommunicationManagerBuilder; 13 + use buttplug_server_hwmgr_lovense_connect::LovenseConnectServiceCommunicationManagerBuilder; 14 + use buttplug_server_hwmgr_websocket::WebsocketServerDeviceCommunicationManagerBuilder; 15 + use buttplug_server::{ 16 + connector::ButtplugRemoteServerConnector, device::{ 17 + ServerDeviceManagerBuilder, 18 + }, message::serializer::ButtplugServerJSONSerializer, ButtplugServerBuilder 19 + }; 20 + use once_cell::sync::OnceCell; 21 + // Device communication manager setup gets its own module because the includes and platform 22 + // specifics are such a mess. 23 + 24 + pub fn setup_server_device_comm_managers( 25 + args: &EngineOptions, 26 + server_builder: &mut ServerDeviceManagerBuilder, 27 + ) { 28 + if args.use_bluetooth_le() { 29 + info!("Including Bluetooth LE (btleplug) Device Comm Manager Support"); 30 + let mut command_manager_builder = BtlePlugCommunicationManagerBuilder::default(); 31 + #[cfg(target_os = "ios")] 32 + command_manager_builder.requires_keepalive(true); 33 + #[cfg(not(target_os = "ios"))] 34 + command_manager_builder.requires_keepalive(false); 35 + server_builder.comm_manager(command_manager_builder); 36 + } 37 + if args.use_lovense_connect() { 38 + info!("Including Lovense Connect App Support"); 39 + server_builder.comm_manager(LovenseConnectServiceCommunicationManagerBuilder::default()); 40 + } 41 + #[cfg(not(any(target_os = "android", target_os = "ios")))] 42 + { 43 + use buttplug_server_hwmgr_hid::HidCommunicationManagerBuilder; 44 + use buttplug_server_hwmgr_lovense_dongle::LovenseHIDDongleCommunicationManagerBuilder; 45 + use buttplug_server_hwmgr_serial::SerialPortCommunicationManagerBuilder; 46 + if args.use_lovense_dongle_hid() { 47 + info!("Including Lovense HID Dongle Support"); 48 + server_builder.comm_manager(LovenseHIDDongleCommunicationManagerBuilder::default()); 49 + } 50 + if args.use_serial_port() { 51 + info!("Including Serial Port Support"); 52 + server_builder.comm_manager(SerialPortCommunicationManagerBuilder::default()); 53 + } 54 + if args.use_hid() { 55 + info!("Including Hid Support"); 56 + server_builder.comm_manager(HidCommunicationManagerBuilder::default()); 57 + } 58 + #[cfg(target_os = "windows")] 59 + { 60 + use buttplug_server_hwmgr_xinput::XInputDeviceCommunicationManagerBuilder; 61 + if args.use_xinput() { 62 + info!("Including XInput Gamepad Support"); 63 + server_builder.comm_manager(XInputDeviceCommunicationManagerBuilder::default()); 64 + } 65 + } 66 + } 67 + if args.use_device_websocket_server() { 68 + info!("Including Websocket Server Device Support"); 69 + let mut builder = 70 + WebsocketServerDeviceCommunicationManagerBuilder::default().listen_on_all_interfaces(true); 71 + if let Some(port) = args.device_websocket_server_port() { 72 + builder = builder.server_port(port); 73 + } 74 + server_builder.comm_manager(builder); 75 + } 76 + } 77 + 78 + pub async fn setup_buttplug_server( 79 + options: &EngineOptions, 80 + backdoor_server: &OnceCell<Arc<BackdoorServer>>, 81 + dcm: &Option<Arc<DeviceConfigurationManager>>, 82 + ) -> Result<ButtplugRemoteServer, IntifaceEngineError> { 83 + let mut dm_builder = if let Some(dcm) = dcm { 84 + ServerDeviceManagerBuilder::new_with_arc(dcm.clone()) 85 + } else { 86 + let mut dcm_builder = load_protocol_configs( 87 + options.device_config_json(), 88 + options.user_device_config_json(), 89 + false, 90 + ) 91 + .map_err(|e| IntifaceEngineError::ButtplugError(e.into()))?; 92 + 93 + ServerDeviceManagerBuilder::new( 94 + dcm_builder 95 + .finish() 96 + .map_err(|e| IntifaceEngineError::ButtplugError(e.into()))?, 97 + ) 98 + }; 99 + 100 + setup_server_device_comm_managers(options, &mut dm_builder); 101 + 102 + let mut server_builder = ButtplugServerBuilder::new( 103 + dm_builder 104 + .finish() 105 + .map_err(|e| IntifaceEngineError::ButtplugServerError(e))?, 106 + ); 107 + server_builder 108 + .name(options.server_name()) 109 + .max_ping_time(options.max_ping_time()); 110 + 111 + let core_server = match server_builder.finish() { 112 + Ok(server) => server, 113 + Err(e) => { 114 + error!("Error starting server: {:?}", e); 115 + return Err(IntifaceEngineError::ButtplugServerError(e)); 116 + } 117 + }; 118 + if backdoor_server 119 + .set(Arc::new(BackdoorServer::new(core_server.device_manager()))) 120 + .is_err() 121 + { 122 + Err( 123 + IntifaceError::new("BackdoorServer already initialized somehow! This should never happen!") 124 + .into(), 125 + ) 126 + } else { 127 + Ok(ButtplugRemoteServer::new(core_server)) 128 + } 129 + } 130 + 131 + pub async fn run_server( 132 + server: &ButtplugRemoteServer, 133 + options: &EngineOptions, 134 + ) -> Result<(), ButtplugServerConnectorError> { 135 + if let Some(port) = options.websocket_port() { 136 + server 137 + .start(ButtplugRemoteServerConnector::< 138 + _, 139 + ButtplugServerJSONSerializer, 140 + >::new( 141 + ButtplugWebsocketServerTransportBuilder::default() 142 + .port(port) 143 + .listen_on_all_interfaces(options.websocket_use_all_interfaces()) 144 + .finish(), 145 + )) 146 + .await 147 + } else if let Some(addr) = options.websocket_client_address() { 148 + server 149 + .start(ButtplugRemoteServerConnector::< 150 + _, 151 + ButtplugServerJSONSerializer, 152 + >::new( 153 + ButtplugWebsocketClientTransport::new_insecure_connector(&addr), 154 + )) 155 + .await 156 + } else { 157 + panic!("Websocket port not set, cannot create transport. Please specify a websocket port in arguments."); 158 + } 159 + }
+215
crates/intiface_engine/src/engine.rs
··· 1 + use crate::{ 2 + backdoor_server::BackdoorServer, 3 + buttplug_server::{run_server, setup_buttplug_server}, 4 + error::IntifaceEngineError, 5 + frontend::{ 6 + frontend_external_event_loop, frontend_server_event_loop, process_messages::EngineMessage, 7 + Frontend, 8 + }, 9 + mdns::IntifaceMdns, 10 + options::EngineOptions, 11 + remote_server::ButtplugRemoteServerEvent, 12 + ButtplugRepeater, 13 + }; 14 + 15 + use buttplug_server_device_config::{DeviceConfigurationManager, save_user_config}; 16 + use futures::{pin_mut, StreamExt}; 17 + use once_cell::sync::OnceCell; 18 + use std::{path::Path, sync::Arc, time::Duration}; 19 + use tokio::{fs, select}; 20 + use tokio_util::sync::CancellationToken; 21 + 22 + #[cfg(debug_assertions)] 23 + pub fn maybe_crash_main_thread(options: &EngineOptions) { 24 + if options.crash_main_thread() { 25 + panic!("Crashing main thread by request"); 26 + } 27 + } 28 + 29 + #[allow(dead_code)] 30 + #[cfg(debug_assertions)] 31 + pub fn maybe_crash_task_thread(options: &EngineOptions) { 32 + if options.crash_task_thread() { 33 + tokio::spawn(async { 34 + tokio::time::sleep(Duration::from_millis(100)).await; 35 + panic!("Crashing a task thread by request"); 36 + }); 37 + } 38 + } 39 + 40 + #[derive(Default)] 41 + pub struct IntifaceEngine { 42 + stop_token: Arc<CancellationToken>, 43 + backdoor_server: OnceCell<Arc<BackdoorServer>>, 44 + } 45 + 46 + impl IntifaceEngine { 47 + pub fn backdoor_server(&self) -> Option<Arc<BackdoorServer>> { 48 + Some(self.backdoor_server.get()?.clone()) 49 + } 50 + 51 + pub async fn run( 52 + &self, 53 + options: &EngineOptions, 54 + frontend: Option<Arc<dyn Frontend>>, 55 + dcm: &Option<Arc<DeviceConfigurationManager>>, 56 + ) -> Result<(), IntifaceEngineError> { 57 + // Set up Frontend 58 + if let Some(frontend) = &frontend { 59 + let frontend_loop = frontend_external_event_loop(frontend.clone(), self.stop_token.clone()); 60 + tokio::spawn(async move { 61 + frontend_loop.await; 62 + }); 63 + 64 + frontend.connect().await.unwrap(); 65 + frontend.send(EngineMessage::EngineStarted {}).await; 66 + } 67 + 68 + // Set up mDNS 69 + let _mdns_server = if options.broadcast_server_mdns() { 70 + // TODO Unregister whenever we have a live connection 71 + 72 + // TODO Support different services for engine versus repeater 73 + Some(IntifaceMdns::new()) 74 + } else { 75 + None 76 + }; 77 + 78 + // Set up Repeater (if in repeater mode) 79 + if options.repeater_mode() { 80 + info!("Starting repeater"); 81 + 82 + let repeater = ButtplugRepeater::new( 83 + options.repeater_local_port().unwrap(), 84 + &options.repeater_remote_address().as_ref().unwrap(), 85 + self.stop_token.child_token(), 86 + ); 87 + select! { 88 + _ = self.stop_token.cancelled() => { 89 + info!("Owner requested process exit, exiting."); 90 + } 91 + _ = repeater.listen() => { 92 + info!("Repeater listener stopped, exiting."); 93 + } 94 + }; 95 + if let Some(frontend) = &frontend { 96 + frontend.send(EngineMessage::EngineStopped {}).await; 97 + tokio::time::sleep(Duration::from_millis(100)).await; 98 + frontend.disconnect(); 99 + } 100 + return Ok(()); 101 + } 102 + 103 + // Set up Engine (if in engine mode) 104 + 105 + // At this point we will have received and validated options. 106 + 107 + // Hang out until those listeners get sick of listening. 108 + info!("Intiface CLI Setup finished, running server tasks until all joined."); 109 + let server = setup_buttplug_server(options, &self.backdoor_server, &dcm).await?; 110 + let dcm = server 111 + .server() 112 + .device_manager() 113 + .device_configuration_manager() 114 + .clone(); 115 + if let Some(config_path) = options.user_device_config_path() { 116 + let stream = server.event_stream(); 117 + { 118 + let config_path = config_path.to_owned(); 119 + tokio::spawn(async move { 120 + pin_mut!(stream); 121 + loop { 122 + if let Some(event) = stream.next().await { 123 + match event { 124 + ButtplugRemoteServerEvent::DeviceAdded { 125 + index: _, 126 + identifier: _, 127 + name: _, 128 + display_name: _, 129 + } => { 130 + if let Ok(config_str) = save_user_config(&dcm) { 131 + // Should probably at least log if we fail to write the config file 132 + let _ = fs::write(&Path::new(&config_path), config_str).await; 133 + } 134 + } 135 + _ => continue, 136 + } 137 + }; 138 + } 139 + }); 140 + } 141 + } 142 + if let Some(frontend) = &frontend { 143 + frontend.send(EngineMessage::EngineServerCreated {}).await; 144 + let event_receiver = server.event_stream(); 145 + let frontend_clone = frontend.clone(); 146 + let stop_child_token = self.stop_token.child_token(); 147 + tokio::spawn(async move { 148 + frontend_server_event_loop(event_receiver, frontend_clone, stop_child_token).await; 149 + }); 150 + } 151 + 152 + loop { 153 + let session_connection_token = CancellationToken::new(); 154 + info!("Starting server"); 155 + 156 + // Let everything spin up, then try crashing. 157 + 158 + #[cfg(debug_assertions)] 159 + maybe_crash_main_thread(options); 160 + 161 + let mut exit_requested = false; 162 + select! { 163 + _ = self.stop_token.cancelled() => { 164 + info!("Owner requested process exit, exiting."); 165 + exit_requested = true; 166 + } 167 + result = run_server(&server, options) => { 168 + match result { 169 + Ok(_) => info!("Connection dropped, restarting stay open loop."), 170 + Err(e) => { 171 + error!("{}", format!("Process Error: {:?}", e)); 172 + 173 + if let Some(frontend) = &frontend { 174 + frontend 175 + .send(EngineMessage::EngineError{ error: format!("Process Error: {:?}", e).to_owned()}) 176 + .await; 177 + } 178 + } 179 + } 180 + } 181 + }; 182 + match server.disconnect().await { 183 + Ok(_) => { 184 + info!("Client forcefully disconnected from server."); 185 + if let Some(frontend) = &frontend { 186 + frontend.send(EngineMessage::ClientDisconnected {}).await; 187 + } 188 + } 189 + Err(_) => info!("Client already disconnected from server."), 190 + }; 191 + session_connection_token.cancel(); 192 + if exit_requested { 193 + info!("Breaking out of event loop in order to exit"); 194 + break; 195 + } 196 + info!("Server connection dropped, restarting"); 197 + } 198 + info!("Shutting down server..."); 199 + if let Err(e) = server.shutdown().await { 200 + error!("Shutdown failed: {:?}", e); 201 + } 202 + info!("Exiting"); 203 + if let Some(frontend) = &frontend { 204 + frontend.send(EngineMessage::EngineStopped {}).await; 205 + tokio::time::sleep(Duration::from_millis(100)).await; 206 + frontend.disconnect(); 207 + } 208 + Ok(()) 209 + } 210 + 211 + pub fn stop(&self) { 212 + info!("Engine stop called, cancelling token."); 213 + self.stop_token.cancel(); 214 + } 215 + }
+54
crates/intiface_engine/src/error.rs
··· 1 + use buttplug_core::errors::ButtplugError; 2 + use buttplug_server::ButtplugServerError; 3 + use std::{error::Error, fmt}; 4 + 5 + #[derive(Debug)] 6 + pub struct IntifaceError { 7 + reason: String, 8 + } 9 + 10 + impl IntifaceError { 11 + pub fn new(error_msg: &str) -> Self { 12 + Self { 13 + reason: error_msg.to_owned(), 14 + } 15 + } 16 + } 17 + 18 + impl fmt::Display for IntifaceError { 19 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 + write!(f, "{}", self.reason) 21 + } 22 + } 23 + 24 + impl Error for IntifaceError { 25 + fn source(&self) -> Option<&(dyn Error + 'static)> { 26 + None 27 + } 28 + } 29 + 30 + #[derive(Debug)] 31 + pub enum IntifaceEngineError { 32 + IoError(std::io::Error), 33 + ButtplugServerError(ButtplugServerError), 34 + ButtplugError(ButtplugError), 35 + IntifaceError(IntifaceError), 36 + } 37 + 38 + impl From<std::io::Error> for IntifaceEngineError { 39 + fn from(err: std::io::Error) -> Self { 40 + IntifaceEngineError::IoError(err) 41 + } 42 + } 43 + 44 + impl From<ButtplugError> for IntifaceEngineError { 45 + fn from(err: ButtplugError) -> Self { 46 + IntifaceEngineError::ButtplugError(err) 47 + } 48 + } 49 + 50 + impl From<IntifaceError> for IntifaceEngineError { 51 + fn from(err: IntifaceError) -> Self { 52 + IntifaceEngineError::IntifaceError(err) 53 + } 54 + }
+134
crates/intiface_engine/src/frontend/mod.rs
··· 1 + pub mod process_messages; 2 + use crate::error::IntifaceError; 3 + use crate::remote_server::ButtplugRemoteServerEvent; 4 + use async_trait::async_trait; 5 + use futures::{pin_mut, Stream, StreamExt}; 6 + pub use process_messages::{EngineMessage, IntifaceMessage}; 7 + use std::sync::Arc; 8 + use tokio::{ 9 + select, 10 + sync::{broadcast, Notify}, 11 + }; 12 + use tokio_util::sync::CancellationToken; 13 + 14 + const VERSION: &str = env!("CARGO_PKG_VERSION"); 15 + 16 + #[async_trait] 17 + pub trait Frontend: Sync + Send { 18 + async fn send(&self, msg: EngineMessage); 19 + async fn connect(&self) -> Result<(), IntifaceError>; 20 + fn disconnect_notifier(&self) -> Arc<Notify>; 21 + fn disconnect(&self); 22 + fn event_stream(&self) -> broadcast::Receiver<IntifaceMessage>; 23 + } 24 + 25 + pub async fn frontend_external_event_loop( 26 + frontend: Arc<dyn Frontend>, 27 + connection_cancellation_token: Arc<CancellationToken>, 28 + ) { 29 + let mut external_receiver = frontend.event_stream(); 30 + loop { 31 + select! { 32 + external_message = external_receiver.recv() => { 33 + match external_message { 34 + Ok(message) => match message { 35 + IntifaceMessage::RequestEngineVersion{expected_version:_} => { 36 + // TODO We should check the version here and shut down on mismatch. 37 + info!("Engine version request received from frontend."); 38 + frontend 39 + .send(EngineMessage::EngineVersion{ version: VERSION.to_owned() }) 40 + .await; 41 + }, 42 + IntifaceMessage::Stop{} => { 43 + connection_cancellation_token.cancel(); 44 + info!("Got external stop request"); 45 + break; 46 + } 47 + }, 48 + Err(_) => { 49 + info!("Frontend sender dropped, assuming connection lost, breaking."); 50 + break; 51 + } 52 + } 53 + }, 54 + _ = connection_cancellation_token.cancelled() => { 55 + info!("Connection cancellation token activated, breaking from frontend external event loop."); 56 + break; 57 + } 58 + } 59 + } 60 + } 61 + 62 + pub async fn frontend_server_event_loop( 63 + receiver: impl Stream<Item = ButtplugRemoteServerEvent>, 64 + frontend: Arc<dyn Frontend>, 65 + connection_cancellation_token: CancellationToken, 66 + ) { 67 + pin_mut!(receiver); 68 + 69 + loop { 70 + select! { 71 + maybe_event = receiver.next() => { 72 + match maybe_event { 73 + Some(event) => match event { 74 + ButtplugRemoteServerEvent::ClientConnected(client_name) => { 75 + info!("Client connected: {}", client_name); 76 + frontend.send(EngineMessage::ClientConnected{client_name}).await; 77 + } 78 + ButtplugRemoteServerEvent::ClientDisconnected => { 79 + info!("Client disconnected."); 80 + frontend 81 + .send(EngineMessage::ClientDisconnected{}) 82 + .await; 83 + } 84 + ButtplugRemoteServerEvent::DeviceAdded { index: device_id, name: device_name, identifier: device_address, display_name: device_display_name } => { 85 + info!("Device Added: {} - {} - {:?}", device_id, device_name, device_address); 86 + frontend 87 + .send(EngineMessage::DeviceConnected { name: device_name, index: device_id, identifier: device_address, display_name: device_display_name }) 88 + .await; 89 + } 90 + ButtplugRemoteServerEvent::DeviceRemoved { index: device_id } => { 91 + info!("Device Removed: {}", device_id); 92 + frontend 93 + .send(EngineMessage::DeviceDisconnected{index: device_id}) 94 + .await; 95 + } 96 + }, 97 + None => { 98 + info!("Lost connection with main thread, breaking."); 99 + break; 100 + }, 101 + } 102 + }, 103 + _ = connection_cancellation_token.cancelled() => { 104 + info!("Connection cancellation token activated, breaking from frontend server event loop"); 105 + break; 106 + } 107 + } 108 + } 109 + info!("Exiting server event receiver loop"); 110 + } 111 + /* 112 + #[derive(Default)] 113 + struct NullFrontend { 114 + notify: Arc<Notify>, 115 + } 116 + 117 + #[async_trait] 118 + impl Frontend for NullFrontend { 119 + async fn send(&self, _: EngineMessage) {} 120 + async fn connect(&self) -> Result<(), IntifaceError> { 121 + Ok(()) 122 + } 123 + fn disconnect(&self) { 124 + self.notify.notify_waiters(); 125 + } 126 + fn disconnect_notifier(&self) -> Arc<Notify> { 127 + self.notify.clone() 128 + } 129 + fn event_stream(&self) -> broadcast::Receiver<IntifaceMessage> { 130 + let (_, receiver) = broadcast::channel(255); 131 + receiver 132 + } 133 + } 134 + */
+40
crates/intiface_engine/src/frontend/process_messages.rs
··· 1 + use buttplug_server_device_config::UserDeviceIdentifier; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + // Everything in this struct is an object, even if it has null contents. This is to make other 5 + // languages happy when trying to recompose JSON into objects. 6 + #[derive(Debug, Clone, Serialize, Deserialize)] 7 + pub enum EngineMessage { 8 + EngineVersion { 9 + version: String, 10 + }, 11 + EngineStarted {}, 12 + EngineError { 13 + error: String, 14 + }, 15 + EngineServerCreated {}, 16 + EngineStopped {}, 17 + ClientConnected { 18 + client_name: String, 19 + }, 20 + ClientDisconnected {}, 21 + DeviceConnected { 22 + name: String, 23 + index: u32, 24 + identifier: UserDeviceIdentifier, 25 + #[serde(skip_serializing_if = "Option::is_none")] 26 + display_name: Option<String>, 27 + }, 28 + DeviceDisconnected { 29 + index: u32, 30 + }, 31 + ClientRejected { 32 + reason: String, 33 + }, 34 + } 35 + 36 + #[derive(Debug, Clone, Serialize, Deserialize)] 37 + pub enum IntifaceMessage { 38 + RequestEngineVersion { expected_version: u32 }, 39 + Stop {}, 40 + }
+18
crates/intiface_engine/src/lib.rs
··· 1 + #[macro_use] 2 + extern crate tracing; 3 + mod backdoor_server; 4 + mod buttplug_server; 5 + mod engine; 6 + mod error; 7 + mod frontend; 8 + mod mdns; 9 + mod options; 10 + mod remote_server; 11 + mod repeater; 12 + pub use backdoor_server::BackdoorServer; 13 + pub use engine::IntifaceEngine; 14 + pub use error::*; 15 + pub use frontend::{EngineMessage, Frontend, IntifaceMessage}; 16 + pub use options::{EngineOptions, EngineOptionsBuilder, EngineOptionsExternal}; 17 + pub use remote_server::{ButtplugRemoteServer, ButtplugServerConnectorError}; 18 + pub use repeater::ButtplugRepeater;
+31
crates/intiface_engine/src/mdns.rs
··· 1 + use rand::distr::{Alphanumeric, SampleString}; 2 + 3 + pub struct IntifaceMdns { 4 + _responder: libmdns::Responder, 5 + _svc: libmdns::Service, 6 + } 7 + 8 + impl IntifaceMdns { 9 + pub fn new() -> Self { 10 + let random_suffix = Alphanumeric.sample_string(&mut rand::rng(), 6); 11 + let instance_name = format!("Intiface {}", random_suffix); 12 + info!( 13 + "Bringing up mDNS Advertisment using instance name {}", 14 + instance_name 15 + ); 16 + 17 + let (_responder, task) = libmdns::Responder::with_default_handle().unwrap(); 18 + let _svc = _responder.register( 19 + "_intiface_engine._tcp".to_owned(), 20 + instance_name, 21 + 12345, 22 + &["path=/"], 23 + ); 24 + tokio::spawn(async move { 25 + info!("Entering up mDNS task"); 26 + task.await; 27 + info!("Exiting mDNS task"); 28 + }); 29 + Self { _responder, _svc } 30 + } 31 + }
+269
crates/intiface_engine/src/options.rs
··· 1 + use getset::{CopyGetters, Getters}; 2 + 3 + #[derive(CopyGetters, Getters, Default, Debug, Clone)] 4 + pub struct EngineOptions { 5 + #[getset(get = "pub")] 6 + device_config_json: Option<String>, 7 + #[getset(get = "pub")] 8 + user_device_config_json: Option<String>, 9 + #[getset(get = "pub")] 10 + user_device_config_path: Option<String>, 11 + #[getset(get = "pub")] 12 + server_name: String, 13 + #[getset(get_copy = "pub")] 14 + websocket_use_all_interfaces: bool, 15 + #[getset(get_copy = "pub")] 16 + websocket_port: Option<u16>, 17 + #[getset(get = "pub")] 18 + websocket_client_address: Option<String>, 19 + #[getset(get_copy = "pub")] 20 + frontend_websocket_port: Option<u16>, 21 + #[getset(get_copy = "pub")] 22 + frontend_in_process_channel: bool, 23 + #[getset(get_copy = "pub")] 24 + max_ping_time: u32, 25 + #[getset(get_copy = "pub")] 26 + use_bluetooth_le: bool, 27 + #[getset(get_copy = "pub")] 28 + use_serial_port: bool, 29 + #[getset(get_copy = "pub")] 30 + use_hid: bool, 31 + #[getset(get_copy = "pub")] 32 + use_lovense_dongle_serial: bool, 33 + #[getset(get_copy = "pub")] 34 + use_lovense_dongle_hid: bool, 35 + #[getset(get_copy = "pub")] 36 + use_xinput: bool, 37 + #[getset(get_copy = "pub")] 38 + use_lovense_connect: bool, 39 + #[getset(get_copy = "pub")] 40 + use_device_websocket_server: bool, 41 + #[getset(get_copy = "pub")] 42 + device_websocket_server_port: Option<u16>, 43 + #[getset(get_copy = "pub")] 44 + crash_main_thread: bool, 45 + #[getset(get_copy = "pub")] 46 + crash_task_thread: bool, 47 + #[getset(get_copy = "pub")] 48 + broadcast_server_mdns: bool, 49 + #[getset(get = "pub")] 50 + mdns_suffix: Option<String>, 51 + #[getset(get_copy = "pub")] 52 + repeater_mode: bool, 53 + #[getset(get_copy = "pub")] 54 + repeater_local_port: Option<u16>, 55 + #[getset(get = "pub")] 56 + repeater_remote_address: Option<String>, 57 + } 58 + 59 + #[derive(Default, Debug, Clone)] 60 + pub struct EngineOptionsExternal { 61 + pub device_config_json: Option<String>, 62 + pub user_device_config_json: Option<String>, 63 + pub user_device_config_path: Option<String>, 64 + pub server_name: String, 65 + pub websocket_use_all_interfaces: bool, 66 + pub websocket_port: Option<u16>, 67 + pub websocket_client_address: Option<String>, 68 + pub frontend_websocket_port: Option<u16>, 69 + pub frontend_in_process_channel: bool, 70 + pub max_ping_time: u32, 71 + pub use_bluetooth_le: bool, 72 + pub use_serial_port: bool, 73 + pub use_hid: bool, 74 + pub use_lovense_dongle_serial: bool, 75 + pub use_lovense_dongle_hid: bool, 76 + pub use_xinput: bool, 77 + pub use_lovense_connect: bool, 78 + pub use_device_websocket_server: bool, 79 + pub device_websocket_server_port: Option<u16>, 80 + pub crash_main_thread: bool, 81 + pub crash_task_thread: bool, 82 + pub broadcast_server_mdns: bool, 83 + pub mdns_suffix: Option<String>, 84 + pub repeater_mode: bool, 85 + pub repeater_local_port: Option<u16>, 86 + pub repeater_remote_address: Option<String>, 87 + } 88 + 89 + impl From<EngineOptionsExternal> for EngineOptions { 90 + fn from(other: EngineOptionsExternal) -> Self { 91 + Self { 92 + device_config_json: other.device_config_json, 93 + user_device_config_json: other.user_device_config_json, 94 + user_device_config_path: other.user_device_config_path, 95 + server_name: other.server_name, 96 + websocket_use_all_interfaces: other.websocket_use_all_interfaces, 97 + websocket_port: other.websocket_port, 98 + websocket_client_address: other.websocket_client_address, 99 + frontend_websocket_port: other.frontend_websocket_port, 100 + frontend_in_process_channel: other.frontend_in_process_channel, 101 + max_ping_time: other.max_ping_time, 102 + use_bluetooth_le: other.use_bluetooth_le, 103 + use_serial_port: other.use_serial_port, 104 + use_hid: other.use_hid, 105 + use_lovense_dongle_serial: other.use_lovense_dongle_serial, 106 + use_lovense_dongle_hid: other.use_lovense_dongle_hid, 107 + use_xinput: other.use_xinput, 108 + use_lovense_connect: other.use_lovense_connect, 109 + use_device_websocket_server: other.use_device_websocket_server, 110 + device_websocket_server_port: other.device_websocket_server_port, 111 + crash_main_thread: other.crash_main_thread, 112 + crash_task_thread: other.crash_task_thread, 113 + broadcast_server_mdns: other.broadcast_server_mdns, 114 + mdns_suffix: other.mdns_suffix, 115 + repeater_mode: other.repeater_mode, 116 + repeater_local_port: other.repeater_local_port, 117 + repeater_remote_address: other.repeater_remote_address, 118 + } 119 + } 120 + } 121 + 122 + #[derive(Default)] 123 + pub struct EngineOptionsBuilder { 124 + options: EngineOptions, 125 + } 126 + 127 + impl EngineOptionsBuilder { 128 + pub fn device_config_json(&mut self, value: &str) -> &mut Self { 129 + self.options.device_config_json = Some(value.to_owned()); 130 + self 131 + } 132 + 133 + pub fn user_device_config_json(&mut self, value: &str) -> &mut Self { 134 + self.options.user_device_config_json = Some(value.to_owned()); 135 + self 136 + } 137 + 138 + pub fn user_device_config_path(&mut self, value: &str) -> &mut Self { 139 + self.options.user_device_config_path = Some(value.to_owned()); 140 + self 141 + } 142 + 143 + pub fn server_name(&mut self, value: &str) -> &mut Self { 144 + self.options.server_name = value.to_owned(); 145 + self 146 + } 147 + 148 + #[cfg(debug_assertions)] 149 + pub fn crash_main_thread(&mut self, value: bool) -> &mut Self { 150 + #[cfg(debug_assertions)] 151 + { 152 + self.options.crash_main_thread = value; 153 + } 154 + self 155 + } 156 + 157 + #[cfg(debug_assertions)] 158 + pub fn crash_task_thread(&mut self, value: bool) -> &mut Self { 159 + #[cfg(debug_assertions)] 160 + { 161 + self.options.crash_main_thread = value; 162 + } 163 + self 164 + } 165 + 166 + pub fn websocket_use_all_interfaces(&mut self, value: bool) -> &mut Self { 167 + self.options.websocket_use_all_interfaces = value; 168 + self 169 + } 170 + 171 + pub fn use_bluetooth_le(&mut self, value: bool) -> &mut Self { 172 + self.options.use_bluetooth_le = value; 173 + self 174 + } 175 + 176 + pub fn use_serial_port(&mut self, value: bool) -> &mut Self { 177 + self.options.use_serial_port = value; 178 + self 179 + } 180 + 181 + pub fn use_hid(&mut self, value: bool) -> &mut Self { 182 + self.options.use_hid = value; 183 + self 184 + } 185 + 186 + pub fn use_lovense_dongle_serial(&mut self, value: bool) -> &mut Self { 187 + self.options.use_lovense_dongle_serial = value; 188 + self 189 + } 190 + 191 + pub fn use_lovense_dongle_hid(&mut self, value: bool) -> &mut Self { 192 + self.options.use_lovense_dongle_hid = value; 193 + self 194 + } 195 + 196 + pub fn use_xinput(&mut self, value: bool) -> &mut Self { 197 + self.options.use_xinput = value; 198 + self 199 + } 200 + 201 + pub fn use_lovense_connect(&mut self, value: bool) -> &mut Self { 202 + self.options.use_lovense_connect = value; 203 + self 204 + } 205 + 206 + pub fn use_device_websocket_server(&mut self, value: bool) -> &mut Self { 207 + self.options.use_device_websocket_server = value; 208 + self 209 + } 210 + 211 + pub fn websocket_port(&mut self, port: u16) -> &mut Self { 212 + self.options.websocket_port = Some(port); 213 + self 214 + } 215 + 216 + pub fn websocket_client_address(&mut self, address: &str) -> &mut Self { 217 + self.options.websocket_client_address = Some(address.to_owned()); 218 + self 219 + } 220 + 221 + pub fn frontend_websocket_port(&mut self, port: u16) -> &mut Self { 222 + self.options.frontend_websocket_port = Some(port); 223 + self 224 + } 225 + 226 + pub fn frontend_in_process_channel(&mut self, value: bool) -> &mut Self { 227 + self.options.frontend_in_process_channel = value; 228 + self 229 + } 230 + 231 + pub fn device_websocket_server_port(&mut self, port: u16) -> &mut Self { 232 + self.options.device_websocket_server_port = Some(port); 233 + self 234 + } 235 + 236 + pub fn max_ping_time(&mut self, value: u32) -> &mut Self { 237 + self.options.max_ping_time = value; 238 + self 239 + } 240 + 241 + pub fn broadcast_server_mdns(&mut self, value: bool) -> &mut Self { 242 + self.options.broadcast_server_mdns = value; 243 + self 244 + } 245 + 246 + pub fn mdns_suffix(&mut self, name: &str) -> &mut Self { 247 + self.options.mdns_suffix = Some(name.to_owned()); 248 + self 249 + } 250 + 251 + pub fn use_repeater_mode(&mut self) -> &mut Self { 252 + self.options.repeater_mode = true; 253 + self 254 + } 255 + 256 + pub fn repeater_local_port(&mut self, port: u16) -> &mut Self { 257 + self.options.repeater_local_port = Some(port); 258 + self 259 + } 260 + 261 + pub fn repeater_remote_address(&mut self, addr: &str) -> &mut Self { 262 + self.options.repeater_remote_address = Some(addr.to_owned()); 263 + self 264 + } 265 + 266 + pub fn finish(&mut self) -> EngineOptions { 267 + self.options.clone() 268 + } 269 + }
+292
crates/intiface_engine/src/remote_server.rs
··· 1 + // Buttplug Rust Source Code File - See https://buttplug.io for more info. 2 + // 3 + // Copyright 2016-2022 Nonpolynomial Labs LLC. All rights reserved. 4 + // 5 + // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 + // for full license information. 7 + 8 + use buttplug_core::{ 9 + connector::ButtplugConnector, 10 + errors::ButtplugError, 11 + message::ButtplugServerMessageV4, 12 + util::{async_manager, stream::convert_broadcast_receiver_to_stream}, 13 + }; 14 + use buttplug_server_device_config::UserDeviceIdentifier; 15 + use buttplug_server::{ 16 + message::{ButtplugClientMessageVariant, ButtplugServerMessageVariant}, ButtplugServer, ButtplugServerBuilder 17 + }; 18 + use futures::{future::Future, pin_mut, select, FutureExt, Stream, StreamExt}; 19 + use getset::Getters; 20 + use serde::{Deserialize, Serialize}; 21 + use std::sync::Arc; 22 + use thiserror::Error; 23 + use tokio::sync::{broadcast, mpsc, Notify}; 24 + 25 + // Clone derived here to satisfy tokio broadcast requirements. 26 + #[derive(Clone, Debug, Serialize, Deserialize)] 27 + pub enum ButtplugRemoteServerEvent { 28 + ClientConnected(String), 29 + ClientDisconnected, 30 + DeviceAdded { 31 + index: u32, 32 + identifier: UserDeviceIdentifier, 33 + name: String, 34 + display_name: Option<String>, 35 + }, 36 + DeviceRemoved { 37 + index: u32, 38 + }, 39 + //DeviceCommand(ButtplugDeviceCommandMessageUnion) 40 + } 41 + 42 + #[derive(Error, Debug)] 43 + pub enum ButtplugServerConnectorError { 44 + #[error("Cannot bring up server for connection: {0}")] 45 + ConnectorError(String), 46 + } 47 + 48 + #[derive(Getters)] 49 + pub struct ButtplugRemoteServer { 50 + #[getset(get = "pub")] 51 + server: Arc<ButtplugServer>, 52 + event_sender: broadcast::Sender<ButtplugRemoteServerEvent>, 53 + disconnect_notifier: Arc<Notify>, 54 + } 55 + 56 + async fn run_device_event_stream( 57 + server: Arc<ButtplugServer>, 58 + remote_event_sender: broadcast::Sender<ButtplugRemoteServerEvent>, 59 + ) { 60 + let server_receiver = server.server_version_event_stream(); 61 + pin_mut!(server_receiver); 62 + loop { 63 + match server_receiver.next().await { 64 + None => { 65 + info!("Server disconnected via server disappearance, exiting loop."); 66 + break; 67 + } 68 + Some(msg) => { 69 + if remote_event_sender.receiver_count() > 0 { 70 + match &msg { 71 + ButtplugServerMessageV4::DeviceAdded(da) => { 72 + if let Some(device_info) = server.device_manager().device_info(da.device_index()) { 73 + let added_event = ButtplugRemoteServerEvent::DeviceAdded { 74 + index: da.device_index(), 75 + name: da.device_name().clone(), 76 + identifier: device_info.identifier().clone().into(), 77 + display_name: device_info.display_name().clone(), 78 + }; 79 + if remote_event_sender.send(added_event).is_err() { 80 + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); 81 + } 82 + } 83 + } 84 + ButtplugServerMessageV4::DeviceRemoved(dr) => { 85 + let removed_event = ButtplugRemoteServerEvent::DeviceRemoved { 86 + index: dr.device_index(), 87 + }; 88 + if remote_event_sender.send(removed_event).is_err() { 89 + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); 90 + } 91 + } 92 + _ => {} 93 + } 94 + } 95 + } 96 + } 97 + } 98 + } 99 + 100 + async fn run_server<ConnectorType>( 101 + server: Arc<ButtplugServer>, 102 + remote_event_sender: broadcast::Sender<ButtplugRemoteServerEvent>, 103 + connector: ConnectorType, 104 + mut connector_receiver: mpsc::Receiver<ButtplugClientMessageVariant>, 105 + disconnect_notifier: Arc<Notify>, 106 + ) where 107 + ConnectorType: 108 + ButtplugConnector<ButtplugServerMessageVariant, ButtplugClientMessageVariant> + 'static, 109 + { 110 + info!("Starting remote server loop"); 111 + let shared_connector = Arc::new(connector); 112 + let server_receiver = server.server_version_event_stream(); 113 + let client_version_receiver = server.event_stream(); 114 + pin_mut!(server_receiver); 115 + pin_mut!(client_version_receiver); 116 + loop { 117 + select! { 118 + connector_msg = connector_receiver.recv().fuse() => match connector_msg { 119 + None => { 120 + info!("Connector disconnected, exiting loop."); 121 + if remote_event_sender.receiver_count() > 0 && remote_event_sender.send(ButtplugRemoteServerEvent::ClientDisconnected).is_err() { 122 + warn!("Cannot update remote about client disconnection"); 123 + } 124 + break; 125 + } 126 + Some(client_message) => { 127 + trace!("Got message from connector: {:?}", client_message); 128 + let server_clone = server.clone(); 129 + let connected = server_clone.connected(); 130 + let connector_clone = shared_connector.clone(); 131 + let remote_event_sender_clone = remote_event_sender.clone(); 132 + async_manager::spawn(async move { 133 + match server_clone.parse_message(client_message.clone()).await { 134 + Ok(ret_msg) => { 135 + // Only send event if we just connected. Sucks to check it on every message but the boolean check should be quick. 136 + if !connected && server_clone.connected() { 137 + if remote_event_sender_clone.receiver_count() > 0 { 138 + if remote_event_sender_clone.send(ButtplugRemoteServerEvent::ClientConnected(server_clone.client_name().unwrap_or("Buttplug Client (No name specified)".to_owned()).clone())).is_err() { 139 + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); 140 + } 141 + } 142 + } 143 + if connector_clone.send(ret_msg).await.is_err() { 144 + error!("Cannot send reply to server, dropping and assuming remote server thread has exited."); 145 + } 146 + }, 147 + Err(err_msg) => { 148 + if connector_clone.send(err_msg.into()).await.is_err() { 149 + error!("Cannot send reply to server, dropping and assuming remote server thread has exited."); 150 + } 151 + } 152 + } 153 + }); 154 + } 155 + }, 156 + _ = disconnect_notifier.notified().fuse() => { 157 + info!("Server disconnected via controller disappearance, exiting loop."); 158 + break; 159 + }, 160 + server_msg = server_receiver.next().fuse() => match server_msg { 161 + None => { 162 + info!("Server disconnected via server disappearance, exiting loop."); 163 + break; 164 + } 165 + Some(msg) => { 166 + if remote_event_sender.receiver_count() > 0 { 167 + match &msg { 168 + ButtplugServerMessageV4::DeviceAdded(da) => { 169 + if let Some(device_info) = server.device_manager().device_info(da.device_index()) { 170 + let added_event = ButtplugRemoteServerEvent::DeviceAdded { index: da.device_index(), name: da.device_name().clone(), identifier: device_info.identifier().clone().into(), display_name: device_info.display_name().clone() }; 171 + if remote_event_sender.send(added_event).is_err() { 172 + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); 173 + } 174 + } 175 + }, 176 + ButtplugServerMessageV4::DeviceRemoved(dr) => { 177 + let removed_event = ButtplugRemoteServerEvent::DeviceRemoved { index: dr.device_index() }; 178 + if remote_event_sender.send(removed_event).is_err() { 179 + error!("Cannot send event to owner, dropping and assuming local server thread has exited."); 180 + } 181 + }, 182 + _ => {} 183 + } 184 + } 185 + } 186 + }, 187 + client_msg = client_version_receiver.next().fuse() => match client_msg { 188 + None => { 189 + info!("Server disconnected via server disappearance, exiting loop."); 190 + break; 191 + } 192 + Some(msg) => { 193 + let connector_clone = shared_connector.clone(); 194 + if connector_clone.send(msg.into()).await.is_err() { 195 + error!("Server disappeared, exiting remote server thread."); 196 + } 197 + } 198 + } 199 + }; 200 + } 201 + if let Err(err) = server.disconnect().await { 202 + error!("Error disconnecting server: {:?}", err); 203 + } 204 + info!("Exiting remote server loop"); 205 + } 206 + 207 + impl Default for ButtplugRemoteServer { 208 + fn default() -> Self { 209 + Self::new( 210 + ButtplugServerBuilder::default() 211 + .finish() 212 + .expect("Default is infallible"), 213 + ) 214 + } 215 + } 216 + 217 + impl ButtplugRemoteServer { 218 + pub fn new(server: ButtplugServer) -> Self { 219 + let (event_sender, _) = broadcast::channel(256); 220 + // Thanks to the existence of the backdoor server, device updates can happen for the lifetime to 221 + // the RemoteServer instance, not just during client connect. We need to make sure these are 222 + // emitted to the frontend. 223 + let server = Arc::new(server); 224 + { 225 + let server = server.clone(); 226 + tokio::spawn({ 227 + let server = server; 228 + let event_sender = event_sender.clone(); 229 + async move { 230 + run_device_event_stream(server, event_sender).await; 231 + } 232 + }); 233 + } 234 + Self { 235 + event_sender, 236 + server: server, 237 + disconnect_notifier: Arc::new(Notify::new()), 238 + } 239 + } 240 + 241 + pub fn event_stream(&self) -> impl Stream<Item = ButtplugRemoteServerEvent> { 242 + convert_broadcast_receiver_to_stream(self.event_sender.subscribe()) 243 + } 244 + 245 + pub fn start<ConnectorType>( 246 + &self, 247 + mut connector: ConnectorType, 248 + ) -> impl Future<Output = Result<(), ButtplugServerConnectorError>> 249 + where 250 + ConnectorType: 251 + ButtplugConnector<ButtplugServerMessageVariant, ButtplugClientMessageVariant> + 'static, 252 + { 253 + let server = self.server.clone(); 254 + let event_sender = self.event_sender.clone(); 255 + let disconnect_notifier = self.disconnect_notifier.clone(); 256 + async move { 257 + let (connector_sender, connector_receiver) = mpsc::channel(256); 258 + // Due to the connect method requiring a mutable connector, we must connect before starting up 259 + // our server loop. Anything that needs to happen outside of the client connection session 260 + // should happen around this. This flow is locked. 261 + connector 262 + .connect(connector_sender) 263 + .await 264 + .map_err(|e| ButtplugServerConnectorError::ConnectorError(format!("{:?}", e)))?; 265 + run_server( 266 + server, 267 + event_sender, 268 + connector, 269 + connector_receiver, 270 + disconnect_notifier, 271 + ) 272 + .await; 273 + Ok(()) 274 + } 275 + } 276 + 277 + pub async fn disconnect(&self) -> Result<(), ButtplugError> { 278 + self.disconnect_notifier.notify_waiters(); 279 + Ok(()) 280 + } 281 + 282 + pub async fn shutdown(&self) -> Result<(), ButtplugError> { 283 + self.server.shutdown().await?; 284 + Ok(()) 285 + } 286 + } 287 + 288 + impl Drop for ButtplugRemoteServer { 289 + fn drop(&mut self) { 290 + self.disconnect_notifier.notify_waiters(); 291 + } 292 + }
+101
crates/intiface_engine/src/repeater.rs
··· 1 + // Is this just two examples from tokio_tungstenite glued together? 2 + // 3 + // It absolute is! 4 + 5 + use futures_util::{future, StreamExt, TryStreamExt}; 6 + use log::info; 7 + use tokio::{ 8 + net::{TcpListener, TcpStream}, 9 + select, 10 + }; 11 + use tokio_tungstenite::connect_async; 12 + use tokio_util::sync::CancellationToken; 13 + 14 + pub struct ButtplugRepeater { 15 + local_port: u16, 16 + remote_address: String, 17 + stop_token: CancellationToken, 18 + } 19 + 20 + impl ButtplugRepeater { 21 + pub fn new(local_port: u16, remote_address: &str, stop_token: CancellationToken) -> Self { 22 + Self { 23 + local_port, 24 + remote_address: remote_address.to_owned(), 25 + stop_token, 26 + } 27 + } 28 + 29 + pub async fn listen(&self) { 30 + info!("Repeater loop starting"); 31 + let addr = format!("127.0.0.1:{}", self.local_port); 32 + 33 + let try_socket = TcpListener::bind(&addr).await; 34 + let listener = try_socket.expect("Failed to bind"); 35 + info!("Listening on: {}", addr); 36 + 37 + loop { 38 + select! { 39 + stream_result = listener.accept() => { 40 + match stream_result { 41 + Ok((stream, _)) => { 42 + let mut remote_address = self.remote_address.clone(); 43 + if !remote_address.starts_with("ws://") { 44 + remote_address.insert_str(0, "ws://"); 45 + } 46 + tokio::spawn(ButtplugRepeater::accept_connection(remote_address, stream)); 47 + }, 48 + Err(e) => { 49 + error!("Error accepting new websocket for repeater: {:?}", e); 50 + break; 51 + } 52 + } 53 + }, 54 + _ = self.stop_token.cancelled() => { 55 + info!("Repeater loop requested to stop, breaking."); 56 + break; 57 + } 58 + } 59 + } 60 + info!("Repeater loop exiting"); 61 + } 62 + 63 + async fn accept_connection(server_addr: String, stream: TcpStream) { 64 + let client_addr = stream 65 + .peer_addr() 66 + .expect("connected streams should have a peer address"); 67 + info!("Client address: {}", client_addr); 68 + 69 + let client_ws_stream = tokio_tungstenite::accept_async(stream) 70 + .await 71 + .expect("Error during the websocket handshake occurred"); 72 + 73 + info!("New WebSocket connection: {}", client_addr); 74 + 75 + info!("Connecting to server {}", server_addr); 76 + 77 + let server_url = url::Url::parse(&server_addr).unwrap(); 78 + 79 + let ws_stream = match connect_async(&server_url).await { 80 + Ok((stream, _)) => stream, 81 + Err(e) => { 82 + error!("Cannot connect: {:?}", e); 83 + return; 84 + } 85 + }; 86 + info!("WebSocket handshake has been successfully completed"); 87 + 88 + let (server_write, server_read) = ws_stream.split(); 89 + 90 + let (client_write, client_read) = client_ws_stream.split(); 91 + 92 + let client_fut = client_read 93 + .try_filter(|msg| future::ready(msg.is_text() || msg.is_binary())) 94 + .forward(server_write); 95 + let server_fut = server_read 96 + .try_filter(|msg| future::ready(msg.is_text() || msg.is_binary())) 97 + .forward(client_write); 98 + future::select(client_fut, server_fut).await; 99 + info!("Closing repeater connection."); 100 + } 101 + }