mirror of
https://gitlab.com/marcandre.lureau/qemu-display.git
synced 2024-12-22 05:35:20 +00:00
qemu-display: start win32 support
This commit is contained in:
parent
ecbeb857c5
commit
1a24e051d0
@ -7,6 +7,7 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0"
|
||||
log = "0.4"
|
||||
derivative = "2.2.0"
|
||||
zbus = { version = "2.0", features = ["xml"] }
|
||||
@ -23,3 +24,7 @@ usbredirhost = "0.0.1"
|
||||
async-broadcast = "0.3.3"
|
||||
async-trait = "0.1.48"
|
||||
async-lock = "2.3.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
uds_windows = "1.0.1"
|
||||
windows = { version = "0.39.0", features = ["Win32_Networking_WinSock", "Win32_Foundation", "Win32_System_IO", "Win32_System_Threading"] }
|
||||
|
@ -1,6 +1,14 @@
|
||||
use std::os::unix::{io::AsRawFd, net::UnixStream};
|
||||
use zbus::{dbus_interface, dbus_proxy, zvariant::Fd, Connection};
|
||||
#[cfg(windows)]
|
||||
use crate::win32::Fd;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
#[cfg(unix)]
|
||||
use zbus::zvariant::Fd;
|
||||
use zbus::{dbus_interface, dbus_proxy, Connection};
|
||||
|
||||
use crate::util;
|
||||
use crate::Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -236,9 +244,8 @@ impl Audio {
|
||||
|
||||
pub async fn register_out_listener<H: AudioOutHandler>(&mut self, handler: H) -> Result<()> {
|
||||
let (p0, p1) = UnixStream::pair()?;
|
||||
self.proxy
|
||||
.register_out_listener(p0.as_raw_fd().into())
|
||||
.await?;
|
||||
let p0 = util::prepare_uds_pass(&p0)?;
|
||||
self.proxy.register_out_listener(p0).await?;
|
||||
let c = zbus::ConnectionBuilder::unix_stream(p1)
|
||||
.p2p()
|
||||
.serve_at(
|
||||
@ -253,9 +260,8 @@ impl Audio {
|
||||
|
||||
pub async fn register_in_listener<H: AudioInHandler>(&mut self, handler: H) -> Result<()> {
|
||||
let (p0, p1) = UnixStream::pair()?;
|
||||
self.proxy
|
||||
.register_in_listener(p0.as_raw_fd().into())
|
||||
.await?;
|
||||
let p0 = util::prepare_uds_pass(&p0)?;
|
||||
self.proxy.register_in_listener(p0).await?;
|
||||
let c = zbus::ConnectionBuilder::unix_stream(p1)
|
||||
.p2p()
|
||||
.serve_at(
|
||||
|
@ -1,14 +1,16 @@
|
||||
#[cfg(windows)]
|
||||
use crate::win32::Fd;
|
||||
use std::convert::TryFrom;
|
||||
use zbus::{
|
||||
dbus_proxy,
|
||||
zvariant::{Fd, ObjectPath},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
use zbus::zvariant::Fd;
|
||||
use zbus::{dbus_proxy, zvariant::ObjectPath};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
#[dbus_proxy(default_service = "org.qemu", interface = "org.qemu.Display1.Chardev")]
|
||||
pub trait Chardev {
|
||||
/// Register method
|
||||
#[cfg(unix)]
|
||||
fn register(&self, stream: Fd) -> zbus::Result<()>;
|
||||
|
||||
/// SendBreak method
|
||||
|
@ -1,15 +1,15 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
convert::TryFrom,
|
||||
os::unix::{io::AsRawFd, net::UnixStream},
|
||||
};
|
||||
use zbus::{
|
||||
dbus_proxy,
|
||||
zvariant::{Fd, ObjectPath},
|
||||
Connection,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use crate::win32::Fd;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::{cell::RefCell, convert::TryFrom};
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
#[cfg(unix)]
|
||||
use zbus::zvariant::Fd;
|
||||
use zbus::{dbus_proxy, zvariant::ObjectPath, Connection};
|
||||
|
||||
use crate::{ConsoleListener, ConsoleListenerHandler, KeyboardProxy, MouseProxy, Result};
|
||||
use crate::{util, ConsoleListener, ConsoleListenerHandler, KeyboardProxy, MouseProxy, Result};
|
||||
|
||||
#[dbus_proxy(default_service = "org.qemu", interface = "org.qemu.Display1.Console")]
|
||||
pub trait Console {
|
||||
@ -87,7 +87,8 @@ impl Console {
|
||||
|
||||
pub async fn register_listener<H: ConsoleListenerHandler>(&self, handler: H) -> Result<()> {
|
||||
let (p0, p1) = UnixStream::pair()?;
|
||||
self.proxy.register_listener(p0.as_raw_fd().into()).await?;
|
||||
let p0 = util::prepare_uds_pass(&p0)?;
|
||||
self.proxy.register_listener(p0).await?;
|
||||
let c = zbus::ConnectionBuilder::unix_stream(p1)
|
||||
.p2p()
|
||||
.serve_at("/org/qemu/Display1/Listener", ConsoleListener::new(handler))?
|
||||
|
@ -1,9 +1,12 @@
|
||||
#[cfg(windows)]
|
||||
use crate::win32::Fd;
|
||||
use derivative::Derivative;
|
||||
use std::{
|
||||
ops::Drop,
|
||||
os::unix::io::{AsRawFd, IntoRawFd, RawFd},
|
||||
};
|
||||
use zbus::{dbus_interface, zvariant::Fd};
|
||||
use std::ops::Drop;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
|
||||
use zbus::dbus_interface;
|
||||
#[cfg(unix)]
|
||||
use zbus::zvariant::Fd;
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
@ -29,6 +32,7 @@ pub struct Update {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug)]
|
||||
pub struct ScanoutDMABUF {
|
||||
pub fd: RawFd,
|
||||
@ -40,6 +44,10 @@ pub struct ScanoutDMABUF {
|
||||
pub y0_top: bool,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Debug)]
|
||||
pub struct ScanoutDMABUF {}
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub struct Cursor {
|
||||
@ -51,6 +59,7 @@ pub struct Cursor {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for ScanoutDMABUF {
|
||||
fn drop(&mut self) {
|
||||
if self.fd >= 0 {
|
||||
@ -61,6 +70,7 @@ impl Drop for ScanoutDMABUF {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl IntoRawFd for ScanoutDMABUF {
|
||||
fn into_raw_fd(mut self) -> RawFd {
|
||||
std::mem::replace(&mut self.fd, -1)
|
||||
@ -148,6 +158,22 @@ impl<H: ConsoleListenerHandler> ConsoleListener<H> {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[dbus_interface(name = "ScanoutDMABUF")]
|
||||
async fn scanout_dmabuf(
|
||||
&mut self,
|
||||
_fd: Fd,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
_stride: u32,
|
||||
_fourcc: u32,
|
||||
_modifier: u64,
|
||||
_y0_top: bool,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[dbus_interface(name = "ScanoutDMABUF")]
|
||||
async fn scanout_dmabuf(
|
||||
&mut self,
|
||||
|
@ -12,7 +12,9 @@ use zbus::{
|
||||
};
|
||||
use zvariant::OwnedObjectPath;
|
||||
|
||||
use crate::{Audio, Chardev, Clipboard, Error, Result, UsbRedir, VMProxy};
|
||||
#[cfg(unix)]
|
||||
use crate::UsbRedir;
|
||||
use crate::{Audio, Chardev, Clipboard, Error, Result, VMProxy};
|
||||
|
||||
struct Inner<'d> {
|
||||
proxy: fdo::ObjectManagerProxy<'d>,
|
||||
@ -144,6 +146,7 @@ impl<'d> Display<'d> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn usbredir(&self) -> UsbRedir {
|
||||
let chardevs = stream::iter(self.chardevs().await)
|
||||
.filter_map(|c| async move {
|
||||
|
@ -1,5 +1,9 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
pub mod util;
|
||||
#[cfg(windows)]
|
||||
mod win32;
|
||||
|
||||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
@ -30,7 +34,9 @@ pub use mouse::*;
|
||||
mod display;
|
||||
pub use display::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod usbredir;
|
||||
#[cfg(unix)]
|
||||
pub use usbredir::UsbRedir;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,19 +1,22 @@
|
||||
use async_broadcast::{broadcast, Receiver, Sender};
|
||||
use async_lock::RwLock;
|
||||
use futures::Stream;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{
|
||||
io::{AsRawFd, RawFd},
|
||||
net::UnixStream,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
default::Default,
|
||||
io::{Read, Write},
|
||||
os::unix::{
|
||||
io::{AsRawFd, RawFd},
|
||||
net::UnixStream,
|
||||
},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
task::{Context, Poll},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
use usbredirhost::{
|
||||
rusb::{self, UsbContext},
|
||||
Device, DeviceHandler, LogLevel,
|
||||
@ -24,6 +27,7 @@ use crate::{Chardev, Error, Result};
|
||||
#[derive(Debug)]
|
||||
struct InnerHandler {
|
||||
#[allow(unused)] // keep the device opened, as rusb doesn't take it
|
||||
#[cfg(unix)]
|
||||
device_fd: Option<zvariant::OwnedFd>,
|
||||
stream: UnixStream,
|
||||
ctxt: rusb::Context,
|
||||
@ -73,6 +77,7 @@ impl DeviceHandler for Handler {
|
||||
fn flush_writes(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[zbus::dbus_proxy(
|
||||
interface = "org.freedesktop.usbredir1",
|
||||
default_service = "org.freedesktop.usbredir1",
|
||||
@ -88,6 +93,7 @@ impl Handler {
|
||||
|
||||
let (dev, device_fd) = match device.open() {
|
||||
Ok(it) => (it, None),
|
||||
#[cfg(unix)]
|
||||
Err(rusb::Error::Access) => {
|
||||
let (bus, dev) = (device.bus_number(), device.address());
|
||||
let sysbus = zbus::Connection::system().await?;
|
||||
@ -120,6 +126,7 @@ impl Handler {
|
||||
|
||||
let handler = Self {
|
||||
inner: Arc::new(Mutex::new(InnerHandler {
|
||||
#[cfg(unix)]
|
||||
device_fd,
|
||||
stream,
|
||||
event,
|
||||
@ -302,6 +309,7 @@ impl Stream for NFreeChannelsStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn fd_poll_readable(fd: RawFd, wait: Option<RawFd>) -> std::io::Result<bool> {
|
||||
let mut fds = vec![libc::pollfd {
|
||||
fd,
|
||||
|
45
qemu-display/src/util.rs
Normal file
45
qemu-display/src/util.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use crate::Result;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{io::AsRawFd, net::UnixStream};
|
||||
#[cfg(unix)]
|
||||
use zbus::zvariant::Fd;
|
||||
#[cfg(windows)]
|
||||
use win32::Fd;
|
||||
|
||||
#[cfg(windows)]
|
||||
use crate::win32;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::io::AsRawSocket;
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Networking::WinSock::{WSADuplicateSocketW, SOCKET, WSAPROTOCOL_INFOW};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::System::Threading::PROCESS_DUP_HANDLE;
|
||||
|
||||
pub fn prepare_uds_pass(us: &UnixStream) -> Result<Fd> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Ok(us.as_raw_fd().into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let pid = win32::unix_stream_get_peer_pid(us)?;
|
||||
let p = win32::ProcessHandle::open(Some(pid), PROCESS_DUP_HANDLE)?;
|
||||
let mut info = unsafe { std::mem::zeroed() };
|
||||
if unsafe { WSADuplicateSocketW(SOCKET(us.as_raw_socket() as _), p.process_id(), &mut info) }
|
||||
!= 0
|
||||
{
|
||||
return Err(crate::Error::Io(win32::wsa_last_err()));
|
||||
}
|
||||
let info = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
&info as *const _ as *const u8,
|
||||
std::mem::size_of::<WSAPROTOCOL_INFOW>(),
|
||||
)
|
||||
};
|
||||
Ok(info.to_vec())
|
||||
}
|
||||
}
|
85
qemu-display/src/win32.rs
Normal file
85
qemu-display/src/win32.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use std::io;
|
||||
use uds_windows::UnixStream;
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
|
||||
|
||||
pub type Fd = Vec<u8>;
|
||||
|
||||
// A process handle
|
||||
pub struct ProcessHandle(HANDLE);
|
||||
|
||||
impl Drop for ProcessHandle {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CloseHandle(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessHandle {
|
||||
// Open the process associated with the process_id (if None, the current process)
|
||||
pub fn open(
|
||||
process_id: Option<u32>,
|
||||
desired_access: PROCESS_ACCESS_RIGHTS,
|
||||
) -> Result<Self, io::Error> {
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcess};
|
||||
|
||||
let process = if let Some(process_id) = process_id {
|
||||
unsafe { OpenProcess(desired_access, false, process_id)? }
|
||||
} else {
|
||||
unsafe { GetCurrentProcess() }
|
||||
};
|
||||
|
||||
Ok(Self(process))
|
||||
}
|
||||
|
||||
pub fn process_id(&self) -> u32 {
|
||||
use windows::Win32::System::Threading::GetProcessId;
|
||||
|
||||
unsafe { GetProcessId(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wsa_last_err() -> io::Error {
|
||||
use windows::Win32::Networking::WinSock::WSAGetLastError;
|
||||
|
||||
let err = unsafe { WSAGetLastError() };
|
||||
io::Error::from_raw_os_error(err.0)
|
||||
}
|
||||
|
||||
// Get the process ID of the connected peer
|
||||
pub fn unix_stream_get_peer_pid(stream: &UnixStream) -> Result<u32, io::Error> {
|
||||
use std::os::windows::io::AsRawSocket;
|
||||
use windows::Win32::Networking::WinSock::{
|
||||
WSAIoctl, IOC_OUT, IOC_VENDOR, SOCKET, SOCKET_ERROR,
|
||||
};
|
||||
|
||||
macro_rules! _WSAIOR {
|
||||
($x:expr, $y:expr) => {
|
||||
IOC_OUT | $x | $y
|
||||
};
|
||||
}
|
||||
|
||||
let socket = stream.as_raw_socket();
|
||||
const SIO_AF_UNIX_GETPEERPID: u32 = _WSAIOR!(IOC_VENDOR, 256);
|
||||
let mut ret = 0 as u32;
|
||||
let mut bytes = 0;
|
||||
|
||||
let r = unsafe {
|
||||
WSAIoctl(
|
||||
SOCKET(socket as _),
|
||||
SIO_AF_UNIX_GETPEERPID,
|
||||
0 as *mut _,
|
||||
0,
|
||||
&mut ret as *mut _ as *mut _,
|
||||
std::mem::size_of_val(&ret) as u32,
|
||||
&mut bytes,
|
||||
0 as *mut _,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
if r == SOCKET_ERROR {
|
||||
return Err(wsa_last_err());
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
@ -17,3 +17,4 @@ rdw = { package = "rdw4", version = "0.1", features = ["bindings"] }
|
||||
futures-util = "0.3"
|
||||
futures = "0.3"
|
||||
async-trait = "0.1"
|
||||
uds_windows = "1.0.2"
|
||||
|
@ -5,6 +5,7 @@ use keycodemap::KEYMAP_XORGEVDEV2QNUM;
|
||||
use once_cell::sync::OnceCell;
|
||||
use qemu_display::{Console, ConsoleListenerHandler};
|
||||
use rdw::{gtk, DisplayExt};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::IntoRawFd;
|
||||
|
||||
mod imp {
|
||||
@ -158,6 +159,11 @@ mod imp {
|
||||
}
|
||||
widget.update_area(u.x as _, u.y as _, u.w as _, u.h as _, u.stride as _, &u.data);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
ScanoutDMABUF(_) => {
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(unix)]
|
||||
ScanoutDMABUF(s) => {
|
||||
widget.set_display_size(Some((s.width as _, s.height as _)));
|
||||
widget.set_dmabuf_scanout(rdw::RdwDmabufScanout {
|
||||
|
@ -2,17 +2,19 @@ use futures_util::StreamExt;
|
||||
use gio::ApplicationFlags;
|
||||
use glib::MainContext;
|
||||
use gtk::{gio, glib, prelude::*};
|
||||
use qemu_display::{Chardev, Console, Display};
|
||||
use qemu_display::{util, Chardev, Console, Display};
|
||||
use rdw::gtk;
|
||||
use std::{cell::RefCell, sync::Arc};
|
||||
|
||||
mod audio;
|
||||
mod clipboard;
|
||||
mod display;
|
||||
#[cfg(unix)]
|
||||
mod usbredir;
|
||||
|
||||
struct Inner {
|
||||
app: gtk::Application,
|
||||
#[cfg(unix)]
|
||||
usbredir: RefCell<Option<usbredir::Handler>>,
|
||||
audio: RefCell<Option<audio::Handler>>,
|
||||
clipboard: RefCell<Option<clipboard::Handler>>,
|
||||
@ -101,6 +103,7 @@ impl App {
|
||||
let app = App {
|
||||
inner: Arc::new(Inner {
|
||||
app,
|
||||
#[cfg(unix)]
|
||||
usbredir: Default::default(),
|
||||
audio: Default::default(),
|
||||
clipboard: Default::default(),
|
||||
@ -176,6 +179,7 @@ impl App {
|
||||
.unwrap()
|
||||
.set_child(Some(&rdw));
|
||||
|
||||
#[cfg(unix)]
|
||||
app_clone.set_usbredir(usbredir::Handler::new(display.usbredir().await));
|
||||
|
||||
if let Ok(Some(audio)) = display.audio().await {
|
||||
@ -197,13 +201,15 @@ impl App {
|
||||
}
|
||||
|
||||
if let Ok(c) = Chardev::new(&conn, "qmp").await {
|
||||
use std::{
|
||||
io::{prelude::*, BufReader},
|
||||
os::unix::{io::AsRawFd, net::UnixStream},
|
||||
};
|
||||
use std::io::{prelude::*, BufReader};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
#[cfg(windows)]
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
let (p0, p1) = UnixStream::pair().unwrap();
|
||||
if c.proxy.register(p1.as_raw_fd().into()).await.is_ok() {
|
||||
let fd = util::prepare_uds_pass(&p1).unwrap();
|
||||
if c.proxy.register(fd).await.is_ok() {
|
||||
let mut reader = BufReader::new(p0.try_clone().unwrap());
|
||||
let mut line = String::new();
|
||||
std::thread::spawn(move || loop {
|
||||
@ -218,22 +224,26 @@ impl App {
|
||||
});
|
||||
});
|
||||
|
||||
let action_usb = gio::SimpleAction::new("usb", None);
|
||||
let app_clone = app.clone();
|
||||
action_usb.connect_activate(move |_, _| {
|
||||
let usbredir = app_clone.inner.usbredir.borrow();
|
||||
if let Some(usbredir) = usbredir.as_ref() {
|
||||
let dialog = gtk::Dialog::new();
|
||||
dialog.set_transient_for(app_clone.inner.app.active_window().as_ref());
|
||||
dialog.set_child(Some(&usbredir.widget()));
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
app.inner.app.add_action(&action_usb);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let action_usb = gio::SimpleAction::new("usb", None);
|
||||
let app_clone = app.clone();
|
||||
action_usb.connect_activate(move |_, _| {
|
||||
let usbredir = app_clone.inner.usbredir.borrow();
|
||||
if let Some(usbredir) = usbredir.as_ref() {
|
||||
let dialog = gtk::Dialog::new();
|
||||
dialog.set_transient_for(app_clone.inner.app.active_window().as_ref());
|
||||
dialog.set_child(Some(&usbredir.widget()));
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
app.inner.app.add_action(&action_usb);
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_usbredir(&self, usbredir: usbredir::Handler) {
|
||||
self.inner.usbredir.replace(Some(usbredir));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user