feat(input): Migrate to moq for input transmission (#38)

## Description

**What issue are you solving (or what feature are you adding) and how
are you doing it?**

We cannot use golang for our input binary as we will be redoing the
Webtransport stack, plus we will have to use CGO in-order to hook into
X11. Like what [neko](https://github.com/m1k1o/neko) does.

However, we could go down the Rust route, where X11 mouse/keyboard
drivers are in pretty, and moq-rs (the MoQ library using Webtransport)
works really well. So, that is what am trying to do here; implement
input using rust.
This commit is contained in:
Wanjohi
2024-05-18 01:51:35 +03:00
committed by GitHub
parent 3a7d404a26
commit c0f573599f
10 changed files with 2265 additions and 10 deletions

191
.github/workflows/warp-input.yml vendored Normal file
View File

@@ -0,0 +1,191 @@
#Tabs not spaces, you moron :)
name: CI for netris:warp-input
on:
pull_request:
paths:
- "warp-input.Dockerfile"
- ".github/workflows/warp-input.yml"
- "bin/input/**"
schedule:
- cron: 0 0 * * * # At the end of everyday
workflow_dispatch:
push:
branches: [main]
paths:
- "warp-input.Dockerfile"
- ".github/workflows/warp-input.yml"
tags:
- v*.*.*
release:
types: [published, created]
env:
REGISTRY: ghcr.io
IMAGE_NAME: wanjohiryan/netris
BASE_TAG_PREFIX: warp-input
concurrency:
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-docker-pr:
name: Build image on pr
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
steps:
-
name: Checkout repo
uses: actions/checkout@v4
with:
submodules: recursive
-
name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build Docker image
uses: docker/build-push-action@v5
with:
file: warp-input.Dockerfile
context: ./
push: false
load: true
tags: netris:warp-input
build-docker-main:
name: Build image on merge to main
if: ${{github.ref == 'refs/heads/main'}}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
-
name: Checkout repo
uses: actions/checkout@v4
with:
submodules: recursive
-
name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Extract Container metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.BASE_TAG_PREFIX }}
#
#tag on release, and a nightly build for 'dev'
tags: |
type=raw,value=nightly,enable={{is_default_branch}}
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
-
name: Build Docker image
uses: docker/build-push-action@v5
with:
file: warp-input.Dockerfile
context: ./
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-warp-input-release:
if: ${{ github.event_name == 'release' }}
defaults:
run:
working-directory: bin/input
strategy:
fail-fast: false
matrix:
settings:
- host: ubuntu-20.04
target: x86_64-unknown-linux-gnu
bundles: appimage
asset_name: warp-input-ubuntu-amd64
- host: windows-latest
target: x86_64-pc-windows-msvc
bundles: msi
asset_name: warp-input-windows-amd64
# - host: macos-latest
# target: x86_64-apple-darwin
# bundles: dmg
# asset_name: warp-input-macos-amd64
# - host: macos-latest
# target: aarch64-apple-darwin
# bundles: dmg
# asset_name: warp-input-macos-apple-silicon
# - host: ubuntu-20.04
# target: x86_64-unknown-linux-musl
# - host: ubuntu-20.04
# target: aarch64-unknown-linux-gnu
# - host: ubuntu-20.04
# target: aarch64-unknown-linux-musl
# - host: ubuntu-20.04
# target: armv7-unknown-linux-gnueabihf
name: Build warp-input on release
runs-on: ${{ matrix.settings.host }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
toolchain: stable
components: clippy, rustfmt
- name: Cache Rust Dependencies
uses: Swatinem/rust-cache@v2
with:
save-if: false
prefix-key: 'v0-rust-deps'
shared-key: ${{ matrix.settings.target }}
- name: Cargo build
run: cargo build --target ${{ matrix.settings.target }} --release
- name: Copy and rename artifacts (Linux)
if: ${{ matrix.settings.host == 'ubuntu-20.04' }}
run: |
cp target/${{ matrix.settings.target }}/release/warp-input ./warp-input
- name: Copy and rename artifacts (Windows)
if: ${{ matrix.settings.host == 'windows-latest' }}
run: |
cp "target/${{ matrix.settings.target }}/release/warp-input.exe" ./warp-input.exe
- name: Copy and rename artifacts (macOS)
if: ${{ matrix.settings.host == 'macos-latest' }}
run: |
cp target/${{ matrix.settings.target }}/release/warp-input ./warp-input
- name: Publish release for (${{ matrix.settings.host }})
if: ${{ matrix.settings.host == 'windows-latest' }}
uses: svenstaro/upload-release-action@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./bin/input/warp-input.exe
asset_name: ${{ matrix.settings.asset_name }}
tag: ${{ github.ref }}
- name: Publish release for (${{ matrix.settings.host }})
if: ${{ matrix.settings.host != 'windows-latest' }}
uses: svenstaro/upload-release-action@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./bin/input/warp-input
asset_name: ${{ matrix.settings.asset_name }}
tag: ${{ github.ref }}

1
bin/input/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1701
bin/input/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
bin/input/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "warp-input"
version = "0.0.1"
edition = "2021"
[dependencies]
anyhow = "1.0.82"
clap = "4.5.4"
enigo = "0.2.1"
env_logger = "0.11.3"
log = "0.4.21"
moq-native = { git = "https://github.com/kixelated/moq-rs", version = "0.1.0" }
moq-transport = { git = "https://github.com/kixelated/moq-rs", version = "0.5.0" }
serde = { version="1.0.202" , features = ["derive"]}
serde_json = "1.0.117"
tokio = "1.37.0"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"

View File

@@ -1,3 +0,0 @@
module github.com/wanjohiryan/netris
go 1.22.2

View File

@@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("hello world")
}

43
bin/input/src/cli.rs Normal file
View File

@@ -0,0 +1,43 @@
use clap::Parser;
use std::{net, path};
use url::Url;
#[derive(Parser, Clone, Debug)]
pub struct Config {
/// Listen for UDP packets on the given address.
#[arg(long, default_value = "[::]:0")]
pub bind: net::SocketAddr,
/// Connect to the given URL starting with https://
#[arg(value_parser = moq_url)]
pub url: Url,
/// Use the TLS root CA at this path, encoded as PEM.
///
/// This value can be provided multiple times for multiple roots.
/// If this is empty, system roots will be used instead
#[arg(long)]
pub tls_root: Vec<path::PathBuf>,
/// Danger: Disable TLS certificate verification.
///
/// Fine for local development, but should be used in caution in production.
#[arg(long)]
pub tls_disable_verify: bool,
/// Publish the current time to the relay, otherwise only subscribe.
// #[arg(long)]
// pub publish: bool,
/// The name of the input track.
#[arg(long, default_value = "netris")]
pub namespace: String,
/// The name of the input track.
#[arg(long, default_value = ".catalog")]
pub track: String,
}
fn moq_url(s: &str) -> Result<Url, String> {
Url::try_from(s).map_err(|e| e.to_string())
}

220
bin/input/src/input.rs Normal file
View File

@@ -0,0 +1,220 @@
use anyhow::Context;
use enigo::{
Axis::Horizontal,
Coordinate::Abs,
Direction::{Press, Release},
Enigo, Keyboard, Mouse, Settings,
};
use moq_transport::serve::{
DatagramsReader, GroupsReader, ObjectsReader, StreamReader, TrackReader, TrackReaderMode,
};
use serde::{Deserialize, Serialize};
pub struct Subscriber {
track: TrackReader,
}
#[derive(Debug, Serialize, Deserialize)]
struct MessageObject {
input_type: String,
delta_y: Option<i32>,
delta_x: Option<i32>,
button: Option<i32>,
key_code: Option<i32>,
}
impl Subscriber {
pub fn new(track: TrackReader) -> Self {
Self { track }
}
pub async fn run(self) -> anyhow::Result<()> {
match self.track.mode().await.context("failed to get mode")? {
TrackReaderMode::Stream(stream) => Self::recv_stream(stream).await,
TrackReaderMode::Groups(groups) => Self::recv_groups(groups).await,
TrackReaderMode::Objects(objects) => Self::recv_objects(objects).await,
TrackReaderMode::Datagrams(datagrams) => Self::recv_datagrams(datagrams).await,
}
}
async fn recv_stream(mut track: StreamReader) -> anyhow::Result<()> {
while let Some(mut group) = track.next().await? {
println!("received a stream");
while let Some(object) = group.read_next().await? {
println!("received a stream 1");
let str = String::from_utf8_lossy(&object);
println!("{}", str);
}
}
Ok(())
}
async fn recv_groups(mut groups: GroupsReader) -> anyhow::Result<()> {
while let Some(mut group) = groups.next().await? {
let base = group
.read_next()
.await
.context("failed to get first object")?
.context("empty group")?;
//TODO: Use this to allow for authorisation (admin, player or guest) etc etc
let _base = String::from_utf8_lossy(&base);
// let json = serde_json::from_str(&str)?;
//TODO: Handle clipboard
let mut enigo = Enigo::new(&Settings::default()).unwrap();
while let Some(object) = group.read_next().await? {
let str = String::from_utf8_lossy(&object);
let parsed: MessageObject = serde_json::from_str(&str)?;
match parsed.input_type.as_str() {
"mouse_move" => {
if let (Some(x), Some(y)) = (parsed.delta_x, parsed.delta_y) {
// println!("Handling mouse_move with delta_x: {}, delta_y: {}", x, y);
enigo.move_mouse(x, y, Abs).unwrap();
}
}
"mouse_key_down" => {
if let Some(button) = parsed.button {
// println!("Handling mouse_key_down with key: {}", button);
if let Some(key) = mouse_key_to_enigo(button) {
enigo.button(key, Press).unwrap();
}
}
}
"mouse_key_up" => {
if let Some(button) = parsed.button {
// println!("Handling mouse_key_up with key: {}", button);
if let Some(key) = mouse_key_to_enigo(button) {
enigo.button(key, Release).unwrap();
}
}
}
"mouse_wheel_up" => {
//TODO: handle vertical scrolling
// println!("Handling mouse_wheel_up with key");
enigo.scroll(-2, Horizontal).unwrap();
}
"mouse_wheel_down" => {
//TODO: handle vertical scrolling
// println!("Handling mouse_wheel_down with key");
enigo.scroll(2, Horizontal).unwrap();
}
"key_up" => {
if let Some(key_code) = parsed.key_code {
// println!("Handling key_up with key: {}", key_code);
if let Some(key) = key_to_enigo(key_code as u8) {
enigo.key(key, Release).unwrap();
}
}
}
"key_down" => {
if let Some(key_code) = parsed.key_code {
// println!("Handling key_down with key: {}", key_code);
if let Some(key) = key_to_enigo(key_code as u8) {
enigo.key(key, Press).unwrap();
}
}
}
_ => {
println!("Unknown input_type: {}", parsed.input_type);
}
}
}
}
Ok(())
}
async fn recv_objects(mut objects: ObjectsReader) -> anyhow::Result<()> {
while let Some(mut object) = objects.next().await? {
let payload = object.read_all().await?;
let str = String::from_utf8_lossy(&payload);
println!("{}", str);
}
Ok(())
}
async fn recv_datagrams(mut datagrams: DatagramsReader) -> anyhow::Result<()> {
while let Some(datagram) = datagrams.read().await? {
let str = String::from_utf8_lossy(&datagram.payload);
println!("{}", str);
}
Ok(())
}
}
pub fn mouse_key_to_enigo(key: i32) -> Option<enigo::Button> {
match key {
0 => Some(enigo::Button::Left),
1 => Some(enigo::Button::Middle),
2 => Some(enigo::Button::Right),
_ => None,
}
}
pub fn key_to_enigo(key: u8) -> Option<enigo::Key> {
match key {
27 => Some(enigo::Key::Escape),
112 => Some(enigo::Key::F1),
113 => Some(enigo::Key::F2),
114 => Some(enigo::Key::F3),
115 => Some(enigo::Key::F4),
116 => Some(enigo::Key::F5),
117 => Some(enigo::Key::F6),
118 => Some(enigo::Key::F7),
119 => Some(enigo::Key::F8),
120 => Some(enigo::Key::F9),
121 => Some(enigo::Key::F10),
122 => Some(enigo::Key::F11),
123 => Some(enigo::Key::F12),
// 19 => Some(enigo::Key::Pause), // Pause
// 97 => Some(enigo::Key::Print), // Print
46 => Some(enigo::Key::Delete),
35 => Some(enigo::Key::End),
192 => Some(enigo::Key::Unicode('`')),
48 => Some(enigo::Key::Unicode('0')),
49 => Some(enigo::Key::Unicode('1')),
50 => Some(enigo::Key::Unicode('2')),
51 => Some(enigo::Key::Unicode('3')),
52 => Some(enigo::Key::Unicode('4')),
53 => Some(enigo::Key::Unicode('5')),
54 => Some(enigo::Key::Unicode('6')),
55 => Some(enigo::Key::Unicode('7')),
56 => Some(enigo::Key::Unicode('8')),
57 => Some(enigo::Key::Unicode('9')),
189 => Some(enigo::Key::Unicode('-')),
187 => Some(enigo::Key::Unicode('=')),
8 => Some(enigo::Key::Backspace),
9 => Some(enigo::Key::Tab),
219 => Some(enigo::Key::Unicode('[')),
221 => Some(enigo::Key::Unicode(']')),
220 => Some(enigo::Key::Unicode('\\')),
20 => Some(enigo::Key::CapsLock),
186 => Some(enigo::Key::Unicode(';')),
222 => Some(enigo::Key::Unicode('\'')),
13 => Some(enigo::Key::Return),
16 => Some(enigo::Key::Shift), // ShiftL
188 => Some(enigo::Key::Unicode(',')),
190 => Some(enigo::Key::Unicode('.')),
191 => Some(enigo::Key::Unicode('/')),
161 => Some(enigo::Key::Shift), // ShiftR
38 => Some(enigo::Key::UpArrow),
17 => Some(enigo::Key::Control), // ControlL
18 => Some(enigo::Key::Alt), // AltL
32 => Some(enigo::Key::Space),
165 => Some(enigo::Key::Alt), // AltR
// 103 => Some(enigo::Key::Menu),
163 => Some(enigo::Key::Control), // ControlR
37 => Some(enigo::Key::LeftArrow),
40 => Some(enigo::Key::DownArrow),
39 => Some(enigo::Key::RightArrow),
// 99 => Some(enigo::Key::Raw(45)), // Insert
34 => Some(enigo::Key::PageDown),
36 => Some(enigo::Key::Home),
33 => Some(enigo::Key::PageUp),
a if a >= 65 && a <= 90 => Some(enigo::Key::Unicode((a - 65 + ('a' as u8)) as char)),
_ => None,
}
}

76
bin/input/src/main.rs Normal file
View File

@@ -0,0 +1,76 @@
use moq_transport::{serve, session::Subscriber};
use moq_native::quic;
use std::net;
use url::Url;
use anyhow::Context;
use clap::Parser;
mod input;
#[derive(Parser, Clone)]
pub struct Cli {
/// Listen for UDP packets on the given address.
#[arg(long, default_value = "[::]:0")]
pub bind: net::SocketAddr,
/// Connect to the given URL starting with https://
#[arg()]
pub url: Url,
/// The TLS configuration.
#[command(flatten)]
pub tls: moq_native::tls::Cli,
// /// Publish the current time to the relay, otherwise only subscribe.
// #[arg(long)]
// pub publish: bool,
/// The name of the input track.
#[arg(long, default_value = "netris")]
pub namespace: String,
/// The name of the input track.
#[arg(long, default_value = ".catalog")]
pub track: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
// Disable tracing so we don't get a bunch of Quinn spam.
let tracer = tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::Level::WARN)
.finish();
tracing::subscriber::set_global_default(tracer).unwrap();
let config = Cli::parse();
let tls = config.tls.load()?;
let quic = quic::Endpoint::new(quic::Config {
bind: config.bind,
tls,
})?;
log::info!("connecting to server: url={}", config.url);
let session = quic.client.connect(&config.url).await?;
let (session, mut subscriber) = Subscriber::connect(session)
.await
.context("failed to create MoQ Transport session")?;
let (prod, sub) = serve::Track::new(config.namespace, config.track).produce();
let input = input::Subscriber::new(sub);
//TODO: Make sure to retry until the input server comes [Use Supervisord for now]
tokio::select! {
res = session.run() => res.context("session error")?,
res = input.run() => res.context("input error")?,
res = subscriber.subscribe(prod) => res.context("failed to subscribe to track")?,
}
Ok(())
}

14
warp-input.Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM rust:bookworm as builder
# Create a build directory and copy over all of the files
WORKDIR /build
COPY ./bin/input/ ./
RUN apt-get update && apt-get install -y libxdo-dev
# Reuse a cache between builds.
# I tried to `cargo install`, but it doesn't seem to work with workspaces.
# There's also issues with the cache mount since it builds into /usr/local/cargo/bin
# We can't mount that without clobbering cargo itself.
# We instead we build the binaries and copy them to the cargo bin directory.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/build/target \
cargo build --target x86_64-unknown-linux-gnu --release && cp target/x86_64-unknown-linux-gnu/release/warp-input /usr/bin/warp-input