Skip to content
Merged
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
62 changes: 62 additions & 0 deletions openless-all/app/scripts/windows-ime-lifecycle-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const appRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const imeRoot = join(appRoot, "windows-ime", "src");
const ipcClient = readFileSync(join(imeRoot, "ipc_client.cpp"), "utf8");
const ipcHeader = readFileSync(join(imeRoot, "ipc_client.h"), "utf8");
const textService = readFileSync(join(imeRoot, "text_service.cpp"), "utf8");
const editSession = readFileSync(join(imeRoot, "edit_session.cpp"), "utf8");

assert.doesNotMatch(
ipcClient,
/FlushFileBuffers\(/,
"IME shutdown must not block the host UI thread waiting for the pipe client",
);
assert.match(
ipcClient,
/WaitForClientDisconnect/,
"IME replies should wait for client disconnect through cancelable overlapped I/O",
);
assert.match(ipcHeader, /HRESULT Start\(/, "IME activation should report pipe server startup failures");
assert.match(
ipcClient,
/WaitForSingleObject\(startup_event_/,
"IME activation must wait for the worker's first named-pipe creation result",
);
assert.match(
ipcClient,
/CreateNamedPipeW[\s\S]*ReportStartupResult/,
"IME worker must report the first CreateNamedPipeW result to activation",
);
assert.match(ipcHeader, /void Run\(\) noexcept/, "IME worker exceptions must not terminate the host process");

assert.doesNotMatch(
textService,
/SendMessageTimeoutW\(/,
"IME worker must not synchronously send a stack request to the owner thread",
);
assert.match(textService, /PostMessageW\(/, "IME worker should post an owned request to the owner thread");
assert.match(
textService,
/WaitForMultipleObjects\(/,
"IME owner-thread and async edit waits should be cancelable during shutdown",
);
assert.match(
textService,
/PeekMessageW[\s\S]*PM_REMOVE/,
"IME shutdown should release queued submit requests before destroying the message window",
);

assert.match(
editSession,
/InterlockedIncrement\(&g_object_count\)/,
"IME edit sessions should keep the COM DLL loaded while TSF holds them",
);
assert.match(
editSession,
/InterlockedDecrement\(&g_object_count\)/,
"IME edit sessions should release the COM DLL lifetime count when destroyed",
);
54 changes: 42 additions & 12 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ use crate::types::{
#[cfg(target_os = "windows")]
use crate::windows_ime_ipc::ImeSubmitTarget;
#[cfg(target_os = "windows")]
use crate::windows_ime_session::{PreparedWindowsImeSession, WindowsImeSessionController};
use crate::windows_ime_session::{
PreparedWindowsImeSession, WindowsImeSessionController, WindowsImeSessionError,
};

mod asr_wiring;
mod capsule_focus;
Expand Down Expand Up @@ -3235,6 +3237,7 @@ async fn insert_with_windows_ime_first(
if should_try_non_tsf_insertion_fallback(
allow_non_tsf_insertion_fallback,
InsertStatus::Failed,
true,
) {
return insert_via_non_tsf_fallback(inner, polished, restore_clipboard, paste_shortcut);
}
Expand All @@ -3249,21 +3252,37 @@ async fn insert_with_windows_ime_first(
target: ime_target,
};

let ime_status = match inner.windows_ime.submit_prepared(&prepared, request).await {
Ok(status) => status,
let (ime_status, outcome_known) = match inner
.windows_ime
.submit_prepared(&prepared, request)
.await
{
Ok(status) => (status, true),
Err(WindowsImeSessionError::OutcomeUnknown(error)) => {
log::warn!(
"[windows-ime] TSF submit outcome is unknown; suppressing automatic fallback: {error}"
);
(InsertStatus::Failed, false)
}
Err(error) => {
log::warn!("[windows-ime] TSF submit failed: {error}");
InsertStatus::Failed
(InsertStatus::Failed, true)
}
};
inner.windows_ime.restore_session(prepared);

if ime_status == InsertStatus::Inserted {
ime_status
} else if should_try_non_tsf_insertion_fallback(allow_non_tsf_insertion_fallback, ime_status) {
} else if should_try_non_tsf_insertion_fallback(
allow_non_tsf_insertion_fallback,
ime_status,
outcome_known,
) {
insert_via_non_tsf_fallback(inner, polished, restore_clipboard, paste_shortcut)
} else {
log::warn!("[windows-ime] TSF did not insert; non-TSF insertion fallback is disabled");
if outcome_known {
log::warn!("[windows-ime] TSF did not insert; non-TSF insertion fallback is disabled");
}
InsertStatus::Failed
}
}
Expand All @@ -3272,8 +3291,9 @@ async fn insert_with_windows_ime_first(
fn should_try_non_tsf_insertion_fallback(
allow_non_tsf_insertion_fallback: bool,
ime_status: InsertStatus,
outcome_known: bool,
) -> bool {
allow_non_tsf_insertion_fallback && ime_status != InsertStatus::Inserted
allow_non_tsf_insertion_fallback && outcome_known && ime_status != InsertStatus::Inserted
}

#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -5613,23 +5633,33 @@ mod tests {
fn non_tsf_insertion_fallback_gate_blocks_only_when_disabled() {
assert!(should_try_non_tsf_insertion_fallback(
true,
InsertStatus::CopiedFallback
InsertStatus::CopiedFallback,
true
));
assert!(should_try_non_tsf_insertion_fallback(
true,
InsertStatus::Failed
InsertStatus::Failed,
true
));
assert!(!should_try_non_tsf_insertion_fallback(
true,
InsertStatus::Inserted
InsertStatus::Inserted,
true
));
assert!(!should_try_non_tsf_insertion_fallback(
false,
InsertStatus::CopiedFallback
InsertStatus::CopiedFallback,
true
));
assert!(!should_try_non_tsf_insertion_fallback(
false,
InsertStatus::Failed
InsertStatus::Failed,
true
));
assert!(!should_try_non_tsf_insertion_fallback(
true,
InsertStatus::Failed,
false
));
}

Expand Down
Loading
Loading