#![cfg(feature = "events")]
use std::time::{Duration, Instant, SystemTime};
use lighty_event::{Event, EventBus, InstanceWindowAppearedEvent};
use crate::instance::INSTANCE_MANAGER;
#[cfg(unix)]
use crate::instance::manager::process_is_running;
const POLL_INTERVAL: Duration = Duration::from_millis(100);
const DETECTION_TIMEOUT: Duration = Duration::from_secs(30);
const ASSUMED_DELAY: Duration = Duration::from_secs(5);
pub(crate) async fn detect_window_appearance(
pid: u32,
instance_name: String,
version: String,
event_bus: EventBus,
) {
let Some(detected) = watch(pid).await else {
lighty_core::trace_debug!("[Launch] Window watcher aborted: PID {} exited", pid);
return;
};
if detected {
lighty_core::trace_info!("[Launch] Window appeared for PID: {}", pid);
} else {
lighty_core::trace_info!(
"[Launch] Assuming window appeared for PID: {} (not observable here)",
pid
);
}
event_bus.emit(Event::InstanceWindowAppeared(InstanceWindowAppearedEvent {
pid,
instance_name,
version,
detected,
timestamp: SystemTime::now(),
}));
}
async fn watch(pid: u32) -> Option<bool> {
let Some(watcher) = platform::watcher() else {
return assume_after_delay(pid).await;
};
let deadline = Instant::now() + DETECTION_TIMEOUT;
loop {
if !still_running(pid) {
return None;
}
if watcher.owns_visible_window(pid) {
return Some(true);
}
if Instant::now() >= deadline {
lighty_core::trace_warn!(
"[Launch] Window detection timed out for PID: {}, assuming it is up",
pid
);
return still_running(pid).then_some(false);
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
async fn assume_after_delay(pid: u32) -> Option<bool> {
tokio::time::sleep(ASSUMED_DELAY).await;
still_running(pid).then_some(false)
}
fn still_running(pid: u32) -> bool {
if !INSTANCE_MANAGER.is_alive(pid) {
return false;
}
#[cfg(unix)]
{
process_is_running(pid)
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(windows)]
mod platform {
use windows::core::BOOL;
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, IsWindowVisible,
};
pub(super) fn watcher() -> Option<Watcher> {
Some(Watcher)
}
pub(super) struct Watcher;
impl Watcher {
pub(super) fn owns_visible_window(&self, pid: u32) -> bool {
struct EnumData {
target_pid: u32,
found: bool,
}
unsafe extern "system" fn enum_window_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
let data = &mut *(lparam.0 as *mut EnumData);
if IsWindowVisible(hwnd).as_bool() {
let mut window_pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut window_pid));
if window_pid == data.target_pid {
data.found = true;
return BOOL(0); }
}
BOOL(1) }
}
let mut data = EnumData {
target_pid: pid,
found: false,
};
unsafe {
let _ = EnumWindows(
Some(enum_window_callback),
LPARAM(&mut data as *mut _ as isize),
);
}
data.found
}
}
}
#[cfg(target_os = "linux")]
mod platform {
use x11rb::connection::{Connection, RequestConnection};
use x11rb::protocol::res::{
query_client_ids, query_version, ClientIdMask, ClientIdSpec, X11_EXTENSION_NAME,
};
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt, MapState, Window};
use x11rb::rust_connection::RustConnection;
pub(super) fn watcher() -> Option<Watcher> {
let (connection, screen) = x11rb::connect(None).ok()?;
let root = connection.setup().roots.get(screen)?.root;
let client_list = intern_atom(&connection, b"_NET_CLIENT_LIST")?;
let wm_pid = intern_atom(&connection, b"_NET_WM_PID")?;
let server_side_pid = has_client_ids(&connection);
Some(Watcher {
connection,
root,
client_list,
wm_pid,
server_side_pid,
})
}
fn has_client_ids(connection: &RustConnection) -> bool {
connection
.extension_information(X11_EXTENSION_NAME)
.ok()
.flatten()
.is_some()
&& query_version(connection, 1, 2)
.ok()
.and_then(|cookie| cookie.reply().ok())
.is_some_and(|version| (version.server_major, version.server_minor) >= (1, 2))
}
fn intern_atom(connection: &RustConnection, name: &[u8]) -> Option<u32> {
Some(connection.intern_atom(false, name).ok()?.reply().ok()?.atom)
}
pub(super) struct Watcher {
connection: RustConnection,
root: Window,
client_list: u32,
wm_pid: u32,
server_side_pid: bool,
}
impl Watcher {
pub(super) fn owns_visible_window(&self, pid: u32) -> bool {
self.managed_windows()
.into_iter()
.find(|&window| self.window_pid(window) == Some(pid))
.is_some_and(|window| self.is_viewable(window))
}
fn managed_windows(&self) -> Vec<Window> {
let Ok(cookie) = self.connection.get_property(
false,
self.root,
self.client_list,
AtomEnum::WINDOW,
0,
u32::MAX,
) else {
return Vec::new();
};
let Ok(reply) = cookie.reply() else {
return Vec::new();
};
reply
.value32()
.map(|windows| windows.collect())
.unwrap_or_default()
}
fn window_pid(&self, window: Window) -> Option<u32> {
self.declared_pid(window)
.or_else(|| self.server_pid(window))
}
fn declared_pid(&self, window: Window) -> Option<u32> {
let reply = self
.connection
.get_property(false, window, self.wm_pid, AtomEnum::CARDINAL, 0, 1)
.ok()?
.reply()
.ok()?;
reply.value32()?.next()
}
fn server_pid(&self, window: Window) -> Option<u32> {
if !self.server_side_pid || window == 0 {
return None;
}
let spec = ClientIdSpec {
client: window,
mask: ClientIdMask::LOCAL_CLIENT_PID,
};
let reply = query_client_ids(&self.connection, &[spec])
.ok()?
.reply()
.ok()?;
reply.ids.iter().find_map(|id| id.value.first().copied())
}
fn is_viewable(&self, window: Window) -> bool {
self.connection
.get_window_attributes(window)
.ok()
.and_then(|cookie| cookie.reply().ok())
.is_some_and(|attributes| attributes.map_state == MapState::VIEWABLE)
}
}
}
#[cfg(not(any(windows, target_os = "linux")))]
mod platform {
pub(super) fn watcher() -> Option<Watcher> {
None
}
pub(super) struct Watcher;
impl Watcher {
pub(super) fn owns_visible_window(&self, _pid: u32) -> bool {
false
}
}
}