From f6fc12de03111406a7502a1e88fdbfe4066e8252 Mon Sep 17 00:00:00 2001 From: Aspect Date: Fri, 6 Jun 2025 13:45:51 -0400 Subject: [PATCH 1/3] WIP: Windows service support --- Cargo.lock | 19 +++++++ Cargo.toml | 2 + src/main.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fac2d46..b29963d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2589,6 +2589,8 @@ dependencies = [ "tokio", "toml", "tower-http", + "windows 0.61.1", + "windows-service", "x11rb", ] @@ -2997,6 +2999,12 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -3174,6 +3182,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-service" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac" +dependencies = [ + "bitflags", + "widestring", + "windows-sys 0.59.0", +] + [[package]] name = "windows-strings" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2640b6c..ccb55ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ x11rb = "0.13.1" [target.'cfg(target_os = "windows")'.dependencies] str0m = { version = "0.8.0", default-features = false, features = ["sha1", "wincrypto"] } +windows-service = "0.8.0" +windows = { version = "0.61.1", features = ["Win32_Foundation", "Win32_System_StationsAndDesktops"] } [target.'cfg(not(target_os = "windows"))'.dependencies] str0m = { version = "0.8.0" } diff --git a/src/main.rs b/src/main.rs index 798257b..c7542d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -444,12 +444,149 @@ fn default_vbv_buf_capacity() -> u32 { 120 } + +#[cfg(target_os = "windows")] +fn main() -> Result<()> { + println!("Starting service"); + windows_service::run()?; + Ok(()) +} + +#[cfg(target_os = "windows")] +mod windows_service { + use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher + }; + + use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, HDESK, OpenInputDesktop, CloseDesktop, SetThreadDesktop, DF_ALLOWOTHERACCOUNTHOOK}; + + use anyhow::Result; + + use std::time::Duration; + use std::ffi::OsString; + use tokio::sync::mpsc; + + const SERVICE_NAME: &str = "Tenebra"; + const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + + pub fn run() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) + } + + define_windows_service!(ffi_service_main, service_main); + + pub fn service_main(_arguments: Vec) { + use std::fs::{create_dir_all, OpenOptions}; + use std::io::Write; + + let _ = create_dir_all("C:\\tenebra_debug"); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("C:\\tenebra_debug\\entry.log") + .unwrap(); + writeln!(file, "service_main called").ok(); + + let mut env_vars = OpenOptions::new() + .create(true) + .append(true) + .open("C:\\tenebra_debug\\env_vars.log").unwrap(); + + // Iterate over the environment variables and write them to the file + for (key, value) in std::env::vars() { + writeln!(env_vars, "{}={}", key, value).ok(); + } + + if let Err(e) = run_service() { + writeln!(file, "Service failed to start: {:?}", e).ok(); + } + } + + pub fn run_service() -> Result<()> { + let (tx, mut rx) = mpsc::channel(1); + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + ServiceControl::Stop => { + tx.try_send(()).unwrap(); + ServiceControlHandlerResult::NoError + } + ServiceControl::UserEvent(_code) => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + #[cfg(target_os = "windows")] + sync_thread_desktop(); + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + tokio::select! { + _ = rx.recv() => Ok(()), + res = crate::entrypoint() => res, + } + })?; + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) + } + + pub fn sync_thread_desktop() -> Option { + unsafe { + let hdesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, false, DESKTOP_ACCESS_FLAGS(0x10000000)).ok()?; + + SetThreadDesktop(hdesk).ok()?; + + CloseDesktop(hdesk).ok()?; + Some(hdesk) + } + } +} + +#[cfg(not(target_os = "windows"))] #[tokio::main] async fn main() -> Result<()> { + entrypoint().await +} + +async fn entrypoint() -> Result<()> { // WinCrypto simplifies build significantly on Windows #[cfg(target_os = "windows")] str0m::config::CryptoProvider::WinCrypto.install_process_default(); + #[cfg(target_os = "windows")] + windows_service::sync_thread_desktop(); + // check if we're behind symmetric NAT if stun::is_symmetric_nat() .await @@ -464,11 +601,19 @@ async fn main() -> Result<()> { gstreamer::init().unwrap(); // get the config path + #[cfg(not(target_os = "windows"))] let mut config_path = dirs::config_dir() .context("Failed to find config directory")? .join("tenebra"); - std::fs::create_dir_all(&config_path).context("Failed to create config directory")?; + #[cfg(not(target_os = "windows"))] config_path.push("config.toml"); + + #[cfg(target_os = "windows")] + let mut config_path = std::path::Path::new("C:\\tenebra.toml"); + + #[cfg(not(target_os = "windows"))] + std::fs::create_dir_all(&config_path).context("Failed to create config directory")?; + if !config_path.exists() { std::fs::write(&config_path, include_bytes!("default.toml")) .context("Failed to write default config")?; From 8dc3c13433bf76a98f19c129333cda50e3ad57a0 Mon Sep 17 00:00:00 2001 From: Aspect Date: Sat, 7 Jun 2025 14:49:56 -0400 Subject: [PATCH 2/3] Implement process starter --- Cargo.toml | 17 ++- src/input.rs | 3 + src/main.rs | 132 ++-------------------- src/windows_service.rs | 241 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 121 deletions(-) create mode 100644 src/windows_service.rs diff --git a/Cargo.toml b/Cargo.toml index ccb55ac..33939de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,19 @@ x11rb = "0.13.1" [target.'cfg(target_os = "windows")'.dependencies] str0m = { version = "0.8.0", default-features = false, features = ["sha1", "wincrypto"] } windows-service = "0.8.0" -windows = { version = "0.61.1", features = ["Win32_Foundation", "Win32_System_StationsAndDesktops"] } +windows = { version = "0.61.1", features = [ + "Win32_Foundation", + "Win32_System_RemoteDesktop", + "Win32_Security", + "Win32_System_Threading", + "Win32_System_Services", + "Win32_System_SystemServices", + "Win32_System_Environment", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Environment", + "Win32_System_StationsAndDesktops", + "Win32_System_SystemServices" +] } [target.'cfg(not(target_os = "windows"))'.dependencies] str0m = { version = "0.8.0" } @@ -52,3 +64,6 @@ bindgen = "0.69.4" [profile.dist] inherits = "release" lto = "thin" + +[profile.release] +debug = true diff --git a/src/input.rs b/src/input.rs index 88b361e..b103b46 100644 --- a/src/input.rs +++ b/src/input.rs @@ -278,6 +278,9 @@ pub fn do_input( let mut held: HashSet = HashSet::new(); while let Some(msg) = rx.blocking_recv() { + #[cfg(target_os = "windows")] + let _ = crate::windows_service::sync_thread_desktop(); + match msg { InputCommand { r#type, diff --git a/src/main.rs b/src/main.rs index c7542d7..9fa65fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,6 +101,10 @@ pub mod keys; mod rtc; mod stun; +// This module contains all code related to Windows service functionality +#[cfg(target_os = "windows")] +pub mod windows_service; + pub struct AppError(anyhow::Error); impl IntoResponse for AppError { @@ -447,130 +451,18 @@ fn default_vbv_buf_capacity() -> u32 { #[cfg(target_os = "windows")] fn main() -> Result<()> { - println!("Starting service"); - windows_service::run()?; - Ok(()) -} - -#[cfg(target_os = "windows")] -mod windows_service { - use windows_service::{ - define_windows_service, - service::{ - ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, - ServiceType, - }, - service_control_handler::{self, ServiceControlHandlerResult}, - service_dispatcher - }; - - use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, HDESK, OpenInputDesktop, CloseDesktop, SetThreadDesktop, DF_ALLOWOTHERACCOUNTHOOK}; - - use anyhow::Result; - - use std::time::Duration; - use std::ffi::OsString; - use tokio::sync::mpsc; - - const SERVICE_NAME: &str = "Tenebra"; - const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; - - pub fn run() -> Result<()> { - service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; - Ok(()) - } - - define_windows_service!(ffi_service_main, service_main); - - pub fn service_main(_arguments: Vec) { - use std::fs::{create_dir_all, OpenOptions}; - use std::io::Write; - - let _ = create_dir_all("C:\\tenebra_debug"); - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open("C:\\tenebra_debug\\entry.log") - .unwrap(); - writeln!(file, "service_main called").ok(); - - let mut env_vars = OpenOptions::new() - .create(true) - .append(true) - .open("C:\\tenebra_debug\\env_vars.log").unwrap(); - - // Iterate over the environment variables and write them to the file - for (key, value) in std::env::vars() { - writeln!(env_vars, "{}={}", key, value).ok(); - } - - if let Err(e) = run_service() { - writeln!(file, "Service failed to start: {:?}", e).ok(); - } - } - - pub fn run_service() -> Result<()> { - let (tx, mut rx) = mpsc::channel(1); - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - ServiceControl::Stop => { - tx.try_send(()).unwrap(); - ServiceControlHandlerResult::NoError - } - ServiceControl::UserEvent(_code) => ServiceControlHandlerResult::NoError, - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - - let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; - status_handle.set_service_status(ServiceStatus { - service_type: SERVICE_TYPE, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; - - #[cfg(target_os = "windows")] - sync_thread_desktop(); - + let args: Vec = std::env::args().collect(); + let option: Option<&str> = args.get(1).map(|s| s.as_str()); + if let Some("--console") = option { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() - .block_on(async move { - tokio::select! { - _ = rx.recv() => Ok(()), - res = crate::entrypoint() => res, - } - })?; - - status_handle.set_service_status(ServiceStatus { - service_type: SERVICE_TYPE, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; - - Ok(()) - } - - pub fn sync_thread_desktop() -> Option { - unsafe { - let hdesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, false, DESKTOP_ACCESS_FLAGS(0x10000000)).ok()?; - - SetThreadDesktop(hdesk).ok()?; - - CloseDesktop(hdesk).ok()?; - Some(hdesk) - } + .block_on(crate::entrypoint())?; + } else { + windows_service::run()?; } + Ok(()) } #[cfg(not(target_os = "windows"))] @@ -585,7 +477,7 @@ async fn entrypoint() -> Result<()> { str0m::config::CryptoProvider::WinCrypto.install_process_default(); #[cfg(target_os = "windows")] - windows_service::sync_thread_desktop(); + let _ = windows_service::sync_thread_desktop(); // check if we're behind symmetric NAT if stun::is_symmetric_nat() diff --git a/src/windows_service.rs b/src/windows_service.rs new file mode 100644 index 0000000..87a9b19 --- /dev/null +++ b/src/windows_service.rs @@ -0,0 +1,241 @@ +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher +}; + +use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, HDESK, OpenInputDesktop, CloseDesktop, SetThreadDesktop, DF_ALLOWOTHERACCOUNTHOOK}; + +use windows::Win32::Foundation::*; + +use windows::Win32::System::RemoteDesktop::WTSGetActiveConsoleSessionId; + +use windows::Win32::System::Threading::{ + CreateProcessAsUserW, GetCurrentProcess, OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, + CREATE_NEW_CONSOLE, CREATE_UNICODE_ENVIRONMENT, +}; + +use windows::Win32::Security::*; + +use windows::Win32::System::Environment::{DestroyEnvironmentBlock, CreateEnvironmentBlock}; + +use windows::core::{PCWSTR, PWSTR}; + +use anyhow::Result; + +use core::ffi::c_void; +use std::cell::Cell; +use std::time::Duration; +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::OsStrExt; + +const SERVICE_NAME: &str = "Tenebra"; +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +struct RAIIGuard { + cleanup: Option, +} + +impl RAIIGuard { + fn new(cleanup: F) -> Self { + RAIIGuard { + cleanup: Some(cleanup), + } + } +} + +impl Drop for RAIIGuard { + fn drop(&mut self) { + if let Some(f) = self.cleanup.take() { + f(); + } + } +} + +pub fn run() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; + Ok(()) +} + +define_windows_service!(ffi_service_main, service_main); + +pub fn service_main(_arguments: Vec) { + use std::io::Write; + let mut file = std::fs::File::create("C:\\tenebra_log.txt").unwrap(); + unsafe { std::env::set_var("RUST_BACKTRACE", "1"); } + if let Err(e) = run_service() { + writeln!(&mut file, "Error: {:?}", e).unwrap(); + } +} + +pub fn run_service() -> Result<()> { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + // There's no need to handle the stop event because the service exists immediately + ServiceControl::Stop => ServiceControlHandlerResult::NoError, + ServiceControl::UserEvent(_code) => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + // Launch the process + unsafe { + + let mut token_handle = HANDLE::default(); + OpenProcessToken( + GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_DUPLICATE | TOKEN_QUERY, + &mut token_handle, + )?; + let _token_guard = RAIIGuard::new(|| { let _ = CloseHandle(token_handle); }); + + // Give ourselves SeTcbPrivilege + let mut luid = LUID::default(); + LookupPrivilegeValueW(None, SE_TCB_NAME, &mut luid)?; + let mut new_privs = TOKEN_PRIVILEGES { + PrivilegeCount: 1, + ..Default::default() + }; + new_privs.Privileges[0].Luid = luid; + new_privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges( + token_handle, + false, // Do not disable all other privileges + Some(&new_privs), + 0, // Buffer length for previous state (not needed) + None, // Pointer to previous state (not needed) + None, // Return length (not needed) + )?; + + let mut new_token_handle = HANDLE::default(); + DuplicateTokenEx( + token_handle, + TOKEN_ACCESS_MASK(windows::Win32::System::SystemServices::MAXIMUM_ALLOWED), + None, + SecurityImpersonation, + TokenPrimary, + &mut new_token_handle, + )?; + let _new_token_guard = RAIIGuard::new(|| { let _ = CloseHandle(new_token_handle); }); + + // Get the session ID of the active user's session + let sid = WTSGetActiveConsoleSessionId(); + if sid == 0xFFFFFFFF { + // error, abort + anyhow::bail!("Bad session ID from WTSGetActiveConsoleSessionId"); + } + SetTokenInformation( + new_token_handle, + TokenSessionId, + &sid as *const u32 as *const c_void, + std::mem::size_of::() as u32, + )?; + + let mut env_block: *mut c_void = std::ptr::null_mut(); + CreateEnvironmentBlock(&mut env_block, Some(new_token_handle), false)?; // Use default env + let _env_block_guard = RAIIGuard::new(|| { let _ = DestroyEnvironmentBlock(env_block); }); + + let mut si: STARTUPINFOW = std::mem::zeroed(); + si.cb = std::mem::size_of::() as u32; + let mut desktop_name: Vec = OsStr::new("winsta0\\default") + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + si.lpDesktop = PWSTR(desktop_name.as_mut_ptr()); + + // Relying on the output of current_exe is NOT a security risk, because an attacker + // cannot swap this executable out for a new executable while the service is running. + // Windows prevents users from deleting the executable of a running service. + let command = format!("{} --console", std::env::current_exe()?.display()); + let mut command_wide: Vec = OsStr::new(&command) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); + CreateProcessAsUserW( + Some(new_token_handle), + None, + Some(PWSTR(command_wide.as_mut_ptr())), + None, + None, + false, + CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE, + Some(env_block), + None, + &si, + &mut pi, + )?; + + let _ = CloseHandle(pi.hProcess); + let _ = CloseHandle(pi.hThread); + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + + +thread_local! { + static CURRENT_DESKTOP: Cell> = const { Cell::new(None) }; +} + +pub fn sync_thread_desktop() -> Result<()> { + unsafe { + let new_desktop = OpenInputDesktop( + DF_ALLOWOTHERACCOUNTHOOK, + false, + DESKTOP_ACCESS_FLAGS(0x10000000), + )?; + + CURRENT_DESKTOP.with(|cell| { + let current = cell.get(); + + let should_switch = match current { + Some(current) if current == new_desktop => { + CloseDesktop(new_desktop).ok(); // Already using it; discard duplicate handle + return Ok(()); + } + Some(old) => { + SetThreadDesktop(new_desktop)?; // Switch first + CloseDesktop(old).ok(); // Then safely close old + cell.set(Some(new_desktop)); + Ok(()) + } + None => { + SetThreadDesktop(new_desktop)?; + cell.set(Some(new_desktop)); + Ok(()) + } + }; + + should_switch + }) + } +} From b8e7aeece149fc1e8aaaef92b33bf69ddbc00ffc Mon Sep 17 00:00:00 2001 From: Aspect Date: Sat, 7 Jun 2025 15:01:33 -0400 Subject: [PATCH 3/3] Update README, remove console window --- README.md | 18 ++++++++++++++---- src/windows_service.rs | 12 ++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 194b044..21e68e0 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,23 @@ Tenebra uses GStreamer to record the screen in a cross-platform way, and to enco [GStreamer Installs](https://gstreamer.freedesktop.org/download/) -To use a Github release, you only need the runtime package. To build Tenebra, you need to install both the development and the runtime packages. +To use a Github release, you only need the runtime package. To build Tenebra, you need to install both the development and the runtime packages. On Windows, GStreamer's bin folder must be added to the PATH. -After the server is built with `cargo build --release`, you may run it: +After the server is built with `cargo build --release`, you may run it. On macOS and Windows, this is as easy as: ``` ./target/release/tenebra ``` +But on Windows, Tenebra must run as a service in order to have the necessary integrity level to interact with all parts of the desktop. First, a service must be registered: +``` +sc create Tenebra binPath= "C:\path\to\tenebra\exe" +``` + +Then, starting Tenebra is as easy as: +``` +sc start Tenebra +``` + However, Tenebra reads from a config file which must be populated before running Tenebra. If it is not populated, Tenebra will fail before copying the default config file to the config file directory. * On **Linux** the config file is at `$XDG_CONFIG_HOME`/tenebra/config.toml or `$HOME`/.config/tenebra/config.toml (e.g. /home/alice/.config/tenebra/config.toml) @@ -56,8 +66,8 @@ On macOS, [VideoToolbox](https://developer.apple.com/documentation/videotoolbox) On Windows, [Media Foundation](https://learn.microsoft.com/en-us/windows/win32/medfound/microsoft-media-foundation-sdk) can be used to perform hardware accelerated H.264 encoding. This can be enabled by setting the `hwencode` property in the config.toml to `true`. The `mfh264enc` GStreamer element must be installed and USABLE. Enable `hwencode` will also automatically enable the use of D3D11 for video format conversion. -## Touch input +## Touch input & pen input -On Linux and Windows, Tenebra has support for receiving and emulating touch events (e.g. from an iPad client). +On Linux and Windows, Tenebra has support for receiving and emulating touch and pen events (e.g. from an iPad client). On Linux, this requires permission to access uinput. Reference your distribution's documentation for details. diff --git a/src/windows_service.rs b/src/windows_service.rs index 87a9b19..9d8ec66 100644 --- a/src/windows_service.rs +++ b/src/windows_service.rs @@ -7,22 +7,15 @@ use windows_service::{ service_control_handler::{self, ServiceControlHandlerResult}, service_dispatcher }; - use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, HDESK, OpenInputDesktop, CloseDesktop, SetThreadDesktop, DF_ALLOWOTHERACCOUNTHOOK}; - use windows::Win32::Foundation::*; - use windows::Win32::System::RemoteDesktop::WTSGetActiveConsoleSessionId; - use windows::Win32::System::Threading::{ CreateProcessAsUserW, GetCurrentProcess, OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, - CREATE_NEW_CONSOLE, CREATE_UNICODE_ENVIRONMENT, + CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, }; - use windows::Win32::Security::*; - use windows::Win32::System::Environment::{DestroyEnvironmentBlock, CreateEnvironmentBlock}; - use windows::core::{PCWSTR, PWSTR}; use anyhow::Result; @@ -96,7 +89,6 @@ pub fn run_service() -> Result<()> { // Launch the process unsafe { - let mut token_handle = HANDLE::default(); OpenProcessToken( GetCurrentProcess(), @@ -177,7 +169,7 @@ pub fn run_service() -> Result<()> { None, None, false, - CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE, + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, Some(env_block), None, &si,