Другое

Обнаружение событий изменения IME в фоновом процессе Windows

Изучите методы обнаружения изменений IME в фоновом процессе Windows без опроса. Рассмотрите альтернативы TSF и хуки событий Windows для консольных приложений.

Как фоновой/демонный процесс может получать события изменения IME (метод ввода) для консольного приложения без опроса, используя TSF (Text Services Framework) на Windows?

Я разрабатываю систему, где консольное приложение (например, Neovim, работающий в Windows Terminal) запускает фоновый демон, который должен обнаруживать изменения IME в событийном режиме без опроса.

Требования:

  • Консольное приложение (Neovim) запускает демон.
  • Демон работает в фоне.
  • Демон получает события изменения IME, когда пользователь переключает:
    • IME окна консоли, или
    • глобальный метод ввода в Windows.
  • Любой из сценариев допустим.

Текущий попытка реализации:
Я использую TSF (Text Services Framework) из Rust со следующим кодом:

rust
use windows::Win32::{System::Com::*, UI::TextServices::*, UI::WindowsAndMessaging::*};
use windows_core::{BOOL, GUID, Interface, implement};

#[implement(ITfActiveLanguageProfileNotifySink)]
struct LangProfileSink;

impl ITfActiveLanguageProfileNotifySink_Impl for LangProfileSink_Impl {
    fn OnActivated(
        &self,
        _clsid: *const GUID,
        _guidprofile: *const GUID,
        _factivated: BOOL,
    ) -> windows::core::Result<()> {
        println!("Language profile activated.");
        Ok(())
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()?;

        let thread_mgr: ITfThreadMgr =
            CoCreateInstance(&CLSID_TF_ThreadMgr, None, CLSCTX_ALL)?;

        thread_mgr.Activate()?;

        let sink: ITfActiveLanguageProfileNotifySink = LangProfileSink.into();
        let source: ITfSource = thread_mgr.cast()?;
        let cookie = source.AdviseSink(
            &ITfActiveLanguageProfileNotifySink::IID,
            &sink
        )?;

        println!("Listening to IME updates...");
        let mut msg = MSG::default();
        while GetMessageW(&mut msg, None, 0, 0).as_bool() {
            TranslateMessage(&msg).ok()?;
            DispatchMessageW(&msg);
        }

        source.UnadviseSink(cookie)?;
        thread_mgr.Deactivate()?;
        CoUninitialize();
    }
    Ok(())
}

Проблема:
Эта реализация никогда не получает события IME при переключении методов ввода внутри Windows Terminal, где запущен cargo run.

Ключевые вопросы:

  1. Можно ли фоновому процессу (демону) получать события изменения IME, относящиеся к другому окну/процессу (например, Windows Terminal)?
  2. Может ли программа без собственного графического окна и без фокуса зарегистрировать TSF‑сокеты, которые срабатывают при изменении глобального IME или IME активного окна?
  3. Если нет, существует ли какой‑то событийный API для обнаружения изменений IME между процессами, или опрос – единственный практический способ для консольных сценариев?

Понимание ограничений TSF

Text Services Framework (TSF) работает в контексте конкретного окна и потока, что создаёт фундаментальные сложности при обнаружении событий из другого процесса. Согласно документации Microsoft по TSF, TSF предоставляет уведомления через интерфейсы, такие как ITfActiveLanguageProfileNotifySink, но они предназначены для работы внутри конкретного менеджера документов и потока.

Ключевые ограничения TSF для фоновых процессов:

  • События TSF обычно привязаны к окну и активному менеджеру документов
  • Фоновые процессы без графического окна испытывают трудности с поддержанием контекста TSF
  • Сокеты событий должны регистрироваться в том же потоке/процессе, который управляет контекстом ввода
  • Межпроцессное взаимодействие TSF требует сложного маршалинга COM и переключения контекста

Как отмечено в записи Bugzilla Mozilla, реализация TSF получает уведомления через интерфейсы, которые реализуют конкретные окна, что объясняет, почему ваш фоновой демон не получает событий от Windows Terminal.

Подходы к обнаружению событий IME в кросс-процессном режиме

Хук событий Windows для изменения фокуса

Одним из возможных подходов является использование SetWinEventHook для обнаружения, когда меняется окно с фокусом, а затем запрос статуса IME этого окна. Согласно обсуждениям на Stack Overflow, можно мониторить событие EVENT_SYSTEM_FOREGROUND:

rust
use windows::Win32::UI::Accessibility::*;
use windows::Win32::UI::WindowsAndMessaging::*;

fn setup_win_event_hook() -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        let hook = SetWinEventHook(
            EVENT_SYSTEM_FOREGROUND,
            EVENT_SYSTEM_FOREGROUND,
            None,
            Some(win_event_proc),
            0,
            0,
            WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
        );
        
        if hook.is_invalid() {
            return Err("Failed to set Windows event hook".into());
        }
        
        // Keep the hook alive
        let _ = std::thread::spawn(move || {
            let mut msg = MSG::default();
            while GetMessageW(&mut msg, None, 0, 0).as_bool() {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
        });
        
        Ok(())
    }
}

Мониторинг событий WMI/CIM

Для системных изменений IME можно использовать события Windows Management Instrumentation (WMI). Как показано в руководстве Bart Pasmans, WMI предоставляет уведомления без опроса:

rust
// Это потребует привязок WMI в Rust
// Концептуальный подход с использованием WMI для мониторинга изменений метода ввода

Альтернативные решения на основе событий

Мониторинг сообщений IME

Windows предоставляет прямые сообщения IME, которые приложения могут получать. Согласно документации Microsoft по сообщениям IME, приложения могут отслеживать:

  • WM_IME_COMPOSITION – изменения строки композиции
  • WM_IME_NOTIFY – общие изменения статуса IME

Для фонового демона вам понадобится:

  1. Создать скрытое окно для получения этих сообщений
  2. Использовать SetWindowsHookEx для внедрения хука в целевой процесс
  3. Мониторить глобальный атом IME или разделяемую память

Аудит создания процессов

Для обнаружения изменений, связанных с IME, можно реализовать аудит создания процессов, как предложено в Stealthbits Technologies. Такой подход отслеживает события создания процессов, которые могут указывать на активность IME:

rust
// Мониторинг событий создания процессов, которые могут быть связаны с сервисами IME

Практические стратегии реализации

Гибридный подход: события + резервный опрос

Самое надёжное решение сочетает мониторинг событий с минимальным опросом в качестве резервного механизма:

  1. Основной: использовать SetWinEventHook для обнаружения изменений окна с фокусом
  2. Вторичный: при изменении окна с фокусом запрашивать статус IME этого окна
  3. Третичный: реализовать периодический опрос, если события не приходят
rust
use windows::Win32::UI::TextServices::*;
use windows::Win32::UI::WindowsAndMessaging::*;

pub struct ImeMonitor {
    event_hook: HHOOK,
    polling_timer: Option<timer::Timer>,
}

impl ImeMonitor {
    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
        let event_hook = unsafe {
            SetWinEventHook(
                EVENT_SYSTEM_FOREGROUND,
                EVENT_SYSTEM_FOREGROUND,
                None,
                Some(win_event_proc),
                0,
                0,
                WINEVENT_OUTOFCONTEXT,
            )?
        };

        Ok(Self {
            event_hook,
            polling_timer: None,
        })
    }

    pub fn start_polling_fallback(&mut self, interval_ms: u64) {
        let (tx, rx) = std::sync::mpsc::channel();
        let timer = timer::Timer::new(tx);
        
        timer.schedule_repeating(std::time::Duration::from_millis(interval_ms));
        
        let _ = std::thread::spawn(move || {
            while let Ok(()) = rx.recv() {
                // Poll IME status
                check_ime_status();
            }
        });
        
        self.polling_timer = Some(timer);
    }
}

Интеграция с консольным приложением

Для сценария Neovim + Windows Terminal рассмотрите:

  1. Named Pipes или IPC: Neovim сигнализирует демону о смене фокуса ввода
  2. Мониторинг классов окон: отслеживать конкретные классы окон, связанные с вашим терминалом
  3. Мониторинг реестра: наблюдать за изменениями, связанными с IME, в реестре

Диагностика текущей реализации

Почему ваша реализация TSF не работает

На основании исследований, несколько проблем, вероятно, мешают вашей реализации TSF получать события:

  1. Проблемы с контекстом потока: TSF требует правильной инициализации потока и обработки сообщений
  2. Связь с окном: фоновые процессы без окон могут не иметь корректного контекста TSF
  3. Требования к фокусу: некоторые события TSF срабатывают только при наличии фокуса в приложении
  4. Аппартмент COM: ваш COINIT_APARTMENTTHREADED может быть неподходящим

Рекомендуемые исправления

  1. Добавьте скрытое окно: создайте скрытое окно для поддержания контекста TSF
  2. Используйте глобальный менеджер потоков: запросите глобальный ITfThreadMgr вместо создания нового
  3. Регистрация системных событий: используйте ITfThreadMgrEx для более широкого охвата
  4. Правильный цикл сообщений: убедитесь, что цикл сообщений работает в главном потоке
rust
// Улучшенная реализация TSF с скрытым окном
use windows::Win32::UI::WindowsAndMessaging::*;

struct HiddenWindow {
    hwnd: HWND,
}

impl HiddenWindow {
    fn new() -> Result<Self, Box<dyn std::error::Error>> {
        unsafe {
            let wc = WNDCLASSW {
                lpfnWndProc: Some(def_window_proc),
                hInstance: GetModuleHandleW(None)?,
                lpszClassName: w!("ImeMonitorHiddenWindow"),
                ..Default::default()
            };
            
            if RegisterClassW(&wc) == 0 {
                return Err("Failed to register window class".into());
            }
            
            let hwnd = CreateWindowExW(
                WINDOW_EX_STYLE::default(),
                w!("ImeMonitorHiddenWindow"),
                w!("Hidden IME Monitor"),
                WINDOW_STYLE::default(),
                0, 0, 0, 0,
                None,
                None,
                GetModuleHandleW(None)?,
                None,
            )?;
            
            Ok(Self { hwnd })
        }
    }
}

Источники

  1. Как фоновой/демонный процесс может получать события изменения IME (input method) для консольного приложения без опроса? (TSF / Windows IME) – Stack Overflow
  2. IME Messages – Win32 apps | Microsoft Learn
  3. Detect active window changed using C# without polling – Stack Overflow
  4. Text Services Framework – Win32 apps | Microsoft Learn
  5. Using CTFTOOL.exe to elevate privileges by leveraging Text Services Framework and mitigation processes and steps – Stealthbits Technologies
  6. How to Monitor Windows Events in Real-Time Using PowerShell – Bart Pasmans
  7. Detecting the IME status of other windows – Microsoft Q&A
  8. Text Services Framework – Wikipedia

Заключение

Создание фонового демона, способного обнаруживать изменения IME без опроса, является сложной задачей, но достижимой через несколько подходов:

  1. Мониторинг окон на основе событий: использовать SetWinEventHook для обнаружения изменений окна с фокусом, затем запрашивать статус IME этого окна
  2. События WMI/CIM: отслеживать системные изменения метода ввода через WMI
  3. Гибридный подход: сочетать мониторинг событий с минимальным опросом в качестве резервного механизма
  4. Скрытое окно TSF: создать скрытое окно для поддержания контекста TSF при фоновой обработке событий

Текущая реализация TSF, скорее всего, не работает из‑за отсутствия правильного контекста окна. Наиболее практичным решением для вашего сценария Neovim + Windows Terminal будет реализация мониторинга окна с фокусом, комбинированная с периодическим опросом, что обеспечивает баланс между отзывчивостью и надёжностью. Для продакшн‑использования рекомендуется использовать гибридную стратегию, при которой события имеют приоритет, а опрос служит резервным механизмом, когда события недоступны.

Авторы
Проверено модерацией
Модерация
Обнаружение событий изменения IME в фоновом процессе Windows