Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions libs/cua-driver/rust/crates/cua-driver/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,23 @@ fn main() {
}
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");

if let Ok(out) = std::process::Command::new("xcode-select").arg("-p").output() {
if let Ok(out) = std::process::Command::new("xcode-select")
.arg("-p")
.output()
{
if out.status.success() {
let xcode_path = String::from_utf8_lossy(&out.stdout).trim().to_string();
for sub in [
"Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx",
"Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx",
"usr/lib/swift/macosx",
"usr/lib/swift-5.5/macosx",
] {
println!("cargo:rustc-link-arg=-Wl,-rpath,{xcode_path}/{sub}");
let runtime_path = std::path::Path::new(&xcode_path).join(sub);
if runtime_path.is_dir() {
println!("cargo:rustc-link-search=native={}", runtime_path.display());
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", runtime_path.display());
}
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions libs/cua-driver/rust/crates/platform-macos/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,23 @@ fn main() {
// its own build.rs, but platform-macos' unit-test binary is linked from
// this package, so it needs the same runtime search paths here.
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
if let Ok(out) = std::process::Command::new("xcode-select").arg("-p").output() {
if let Ok(out) = std::process::Command::new("xcode-select")
.arg("-p")
.output()
{
if out.status.success() {
let xcode_path = String::from_utf8_lossy(&out.stdout).trim().to_string();
for sub in [
"Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx",
"Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx",
"usr/lib/swift/macosx",
"usr/lib/swift-5.5/macosx",
] {
println!("cargo:rustc-link-arg=-Wl,-rpath,{xcode_path}/{sub}");
let runtime_path = std::path::Path::new(&xcode_path).join(sub);
if runtime_path.is_dir() {
println!("cargo:rustc-link-search=native={}", runtime_path.display());
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", runtime_path.display());
}
}
}
}
Expand Down
59 changes: 57 additions & 2 deletions libs/cua-driver/rust/crates/platform-macos/src/apps/nsworkspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! selector which `objc2-foundation 0.2.2` does not bind natively — we
//! hand-roll the binding in [`apple_event`] via `extern_methods!`.

use std::mem::{size_of, MaybeUninit};
use std::ptr::NonNull;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
Expand Down Expand Up @@ -494,8 +495,10 @@ fn running_application_info(application: &NSRunningApplication) -> RunningApplic
let executable_path = unsafe { application.executableURL() }
.and_then(|url| unsafe { url.path() })
.map(|path| path.to_string());
let process_generation = unsafe { application.launchDate() }
.map(|date| unsafe { date.timeIntervalSince1970() }.to_bits());
let process_generation = process_generation(pid).or_else(|| {
unsafe { application.launchDate() }
.and_then(|date| generation_from_epoch_seconds(unsafe { date.timeIntervalSince1970() }))
});
RunningApplicationInfo {
pid,
name,
Expand All @@ -510,6 +513,45 @@ fn running_application_info(application: &NSRunningApplication) -> RunningApplic
}
}

/// Exact live-process generation derived from the kernel's BSD process row.
///
/// `NSRunningApplication.launchDate` is legitimately nil for long-lived
/// regular processes including Finder and Docker Desktop. `proc_pidinfo`
/// exposes the process start timestamp for those same PIDs, so use that as
/// the canonical identity and retain launchDate only as a late-notification
/// fallback after the kernel row has disappeared.
pub(crate) fn process_generation(pid: i32) -> Option<u64> {
let mut info = MaybeUninit::<libc::proc_bsdinfo>::zeroed();
let expected = size_of::<libc::proc_bsdinfo>();
let written = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
i32::try_from(expected).ok()?,
)
};
if usize::try_from(written).ok()? != expected {
return None;
}
let info = unsafe { info.assume_init() };
if info.pbi_pid != u32::try_from(pid).ok()? || info.pbi_start_tvsec == 0 {
return None;
}
info.pbi_start_tvsec
.checked_mul(1_000_000)?
.checked_add(info.pbi_start_tvusec)
}

fn generation_from_epoch_seconds(seconds: f64) -> Option<u64> {
if !seconds.is_finite() || seconds <= 0.0 {
return None;
}
let micros = (seconds * 1_000_000.0).round();
(micros <= u64::MAX as f64).then_some(micros as u64)
}

fn install_workspace_observers(hub: &Arc<WorkspaceEventHub>) {
install_workspace_observer(
hub,
Expand Down Expand Up @@ -608,3 +650,16 @@ mod apple_event {
}
}
}

#[cfg(test)]
mod tests {
use super::process_generation;

#[test]
fn live_process_generation_is_kernel_backed_and_stable() {
let pid = i32::try_from(std::process::id()).unwrap();
let first = process_generation(pid).expect("current process must have a BSD start time");
let second = process_generation(pid).expect("current process must remain readable");
assert_eq!(first, second);
}
}
47 changes: 47 additions & 0 deletions libs/cua-driver/rust/crates/platform-macos/src/ax/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,53 @@ pub unsafe fn copy_cf_range_attr(
}
}

/// Copy a `kAXValueCGPointType` attribute. Missing and unsupported attributes
/// are optional; query and type failures remain distinguishable to callers
/// that need an exact read.
pub unsafe fn copy_point_attr(
element: AXUIElementRef,
attr_name: &str,
) -> Result<Option<(f64, f64)>, AXError> {
let attr = CFStr::new(attr_name);
let mut value: CFTypeRef = std::ptr::null();
let err = AXUIElementCopyAttributeValue(element, attr.as_concrete_TypeRef(), &mut value);
if err == kAXErrorNoValue || err == kAXErrorAttributeUnsupported {
return Ok(None);
}
if err != kAXErrorSuccess {
return Err(err);
}
if value.is_null() {
return Ok(None);
}
if core_foundation::base::CFGetTypeID(value) != AXValueGetTypeID() {
CFRelease(value);
return Err(kAXErrorFailure);
}
let value = value as AXValueRef;
if AXValueGetType(value) != kAXValueCGPointType {
CFRelease(value as CFTypeRef);
return Err(kAXErrorFailure);
}
#[repr(C)]
struct CGPoint {
x: f64,
y: f64,
}
let mut point = CGPoint { x: 0.0, y: 0.0 };
let copied = AXValueGetValue(
value,
kAXValueCGPointType,
&mut point as *mut _ as *mut c_void,
);
CFRelease(value as CFTypeRef);
if copied {
Ok(Some((point.x, point.y)))
} else {
Err(kAXErrorFailure)
}
}

/// Set a `kAXValueCFRangeType` attribute.
pub unsafe fn set_cf_range_attr(
element: AXUIElementRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,10 @@ pub(crate) fn keyboard_capability_cells(os_version: &str) -> Vec<CapabilityCell>
WindowStateKind::Visible | WindowStateKind::Occluded
if matches!(
framework,
Framework::AppKit | Framework::Chromium | Framework::WebKit
Framework::Unknown
| Framework::AppKit
| Framework::Chromium
| Framework::WebKit
) =>
{
RouteDecision::Supported {
Expand Down Expand Up @@ -1481,6 +1484,21 @@ mod tests {
.filter(|cell| {
cell.key.action == ActionKind::TypeText && cell.key.framework == Framework::Unknown
})
.all(|cell| matches!(cell.decision, RouteDecision::Unsupported { .. })));
.all(|cell| {
matches!(
(&cell.key.window_state, &cell.decision),
(
WindowStateKind::Visible | WindowStateKind::Occluded,
RouteDecision::Supported {
route: Route::Semantic
}
) | (
WindowStateKind::Minimized
| WindowStateKind::OffSpace
| WindowStateKind::Unknown,
RouteDecision::Unsupported { .. }
)
)
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub(crate) fn pointer_capability_cells(os_version: &str) -> Vec<CapabilityCell>
for state in &states {
let proven_click = os_version == "26.5.1"
&& architecture == "arm64"
&& framework == Framework::Chromium
&& framework != Framework::Catalyst
&& *action == ActionKind::Click
&& *state == WindowStateKind::Visible;
let decision = if proven_click {
Expand Down Expand Up @@ -174,19 +174,18 @@ mod tests {
}

#[test]
fn targeted_pointer_cells_publish_only_the_proven_chromium_click_posture() {
fn targeted_pointer_cells_publish_the_exact_click_posture_without_framework_guessing() {
let cells = pointer_capability_cells("26.5.1");
assert_eq!(cells.len(), 90);
let supported: Vec<_> = cells
.iter()
.filter(|cell| matches!(cell.decision, RouteDecision::Supported { .. }))
.collect();
if std::env::consts::ARCH == "aarch64" {
assert_eq!(supported.len(), 1);
assert_eq!(supported.len(), 5);
assert!(supported.iter().all(|cell| {
cell.key.action == ActionKind::Click
&& cell.key.addressing == AddressingMode::CapturedPoint
&& cell.key.framework == Framework::Chromium
&& cell.key.window_state == WindowStateKind::Visible
&& matches!(
cell.decision,
Expand All @@ -195,6 +194,15 @@ mod tests {
}
)
}));
assert!(supported
.iter()
.any(|cell| cell.key.framework == Framework::Unknown));
assert!(cells.iter().any(|cell| {
cell.key.action == ActionKind::Click
&& cell.key.framework == Framework::Catalyst
&& cell.key.window_state == WindowStateKind::Visible
&& matches!(cell.decision, RouteDecision::Unsupported { .. })
}));
assert!(cells.iter().any(|cell| {
cell.key.action == ActionKind::Click
&& cell.key.framework == Framework::Chromium
Expand Down
Loading
Loading