fix: harden Windows IME review edge cases
This commit is contained in:
1 parent
31045f7871
commit
2554259d80
5 files changed
+184
-32
No files matched your search
@@ -34,8 +34,16 @@ function Test-IsAdministrator {
|
||||
}
|
||||
|
||||
$appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
$registrationRoot = Join-Path $appRoot "src-tauri\target\windows-ime-register"
|
||||
$stagingStamp = "{0:yyyyMMddHHmmss}-{1}" -f (Get-Date), $PID
|
||||
$stagingRoot = Join-Path $appRoot "src-tauri\target\windows-ime-register\$stagingStamp"
|
||||
$stagingRoot = Join-Path $registrationRoot $stagingStamp
|
||||
$registrationManifest = Join-Path $registrationRoot "active-registration.json"
|
||||
$registeredDlls = [ordered]@{}
|
||||
|
||||
function Save-RegistrationManifest {
|
||||
New-Item -ItemType Directory -Path $registrationRoot -Force | Out-Null
|
||||
$registeredDlls | ConvertTo-Json | Set-Content -Path $registrationManifest -Encoding UTF8
|
||||
}
|
||||
|
||||
function Get-DllPath {
|
||||
param(
|
||||
@@ -74,5 +82,7 @@ foreach ($platform in @("x64", "Win32")) {
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "$platform regsvr32 failed with exit code $($process.ExitCode)"
|
||||
}
|
||||
$registeredDlls[$platform] = $dll
|
||||
Save-RegistrationManifest
|
||||
Write-Host "[ok] OpenLess TSF IME registered ($platform)"
|
||||
}
|
||||
@@ -34,6 +34,64 @@ function Test-IsAdministrator {
|
||||
}
|
||||
|
||||
$appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
$registrationRoot = Join-Path $appRoot "src-tauri\target\windows-ime-register"
|
||||
$registrationManifest = Join-Path $registrationRoot "active-registration.json"
|
||||
|
||||
function Get-ManifestDllPath {
|
||||
param(
|
||||
[ValidateSet("x64", "Win32")]
|
||||
[string]$Platform
|
||||
)
|
||||
|
||||
if (-not (Test-Path $registrationManifest)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$manifest = Get-Content -Raw -Path $registrationManifest | ConvertFrom-Json
|
||||
$property = $manifest.PSObject.Properties[$Platform]
|
||||
if ($null -ne $property -and -not [string]::IsNullOrWhiteSpace($property.Value)) {
|
||||
return $property.Value
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[warn] Failed to read OpenLess IME registration manifest: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-LatestStagedDllPath {
|
||||
param(
|
||||
[ValidateSet("x64", "Win32")]
|
||||
[string]$Platform
|
||||
)
|
||||
|
||||
if (-not (Test-Path $registrationRoot)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$folder = if ($Platform -eq "Win32") { "x86" } else { $Platform }
|
||||
$dll = Get-ChildItem -Path $registrationRoot -Filter OpenLessIme.dll -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like "*\$folder\$Configuration\OpenLessIme.dll" } |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($null -ne $dll) {
|
||||
return $dll.FullName
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-LegacyDllPath {
|
||||
param(
|
||||
[ValidateSet("x64", "Win32")]
|
||||
[string]$Platform
|
||||
)
|
||||
|
||||
$legacyFolder = if ($Platform -eq "Win32") { "Win32" } else { $Platform }
|
||||
return Join-Path $appRoot "windows-ime\$legacyFolder\$Configuration\OpenLessIme.dll"
|
||||
}
|
||||
|
||||
function Get-DllPath {
|
||||
param(
|
||||
@@ -41,8 +99,17 @@ function Get-DllPath {
|
||||
[string]$Platform
|
||||
)
|
||||
|
||||
$folder = if ($Platform -eq "Win32") { "Win32" } else { $Platform }
|
||||
return Join-Path $appRoot "windows-ime\$folder\$Configuration\OpenLessIme.dll"
|
||||
$manifestDll = Get-ManifestDllPath $Platform
|
||||
if ($null -ne $manifestDll) {
|
||||
return $manifestDll
|
||||
}
|
||||
|
||||
$stagedDll = Get-LatestStagedDllPath $Platform
|
||||
if ($null -ne $stagedDll) {
|
||||
return $stagedDll
|
||||
}
|
||||
|
||||
return Get-LegacyDllPath $Platform
|
||||
}
|
||||
|
||||
if (-not (Test-IsAdministrator)) {
|
||||
@@ -63,3 +130,7 @@ foreach ($platform in @("x64", "Win32")) {
|
||||
}
|
||||
Write-Host "[ok] OpenLess TSF IME unregistered ($platform)"
|
||||
}
|
||||
|
||||
if (Test-Path $registrationManifest) {
|
||||
Remove-Item -LiteralPath $registrationManifest -Force
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const scriptPath = join(scriptsDir, "windows-package-msvc.ps1");
|
||||
const launcherPath = join(scriptsDir, "windows-package-msvc.cmd");
|
||||
const imeBuildPath = join(scriptsDir, "windows-ime-build.ps1");
|
||||
const imeRegisterPath = join(scriptsDir, "windows-ime-register.ps1");
|
||||
const imeUnregisterPath = join(scriptsDir, "windows-ime-unregister.ps1");
|
||||
const imeSolutionPath = join(appRoot, "windows-ime", "OpenLessIme.sln");
|
||||
const imeProjectPath = join(appRoot, "windows-ime", "OpenLessIme.vcxproj");
|
||||
const imeEditSessionPath = join(appRoot, "windows-ime", "src", "edit_session.cpp");
|
||||
@@ -20,6 +21,7 @@ const script = readFileSync(scriptPath, "utf8");
|
||||
const launcher = readFileSync(launcherPath, "utf8");
|
||||
const imeBuild = readFileSync(imeBuildPath, "utf8");
|
||||
const imeRegister = readFileSync(imeRegisterPath, "utf8");
|
||||
const imeUnregister = readFileSync(imeUnregisterPath, "utf8");
|
||||
const imeSolution = readFileSync(imeSolutionPath, "utf8");
|
||||
const imeProject = readFileSync(imeProjectPath, "utf8");
|
||||
const imeEditSession = readFileSync(imeEditSessionPath, "utf8");
|
||||
@@ -71,6 +73,11 @@ assert.match(imeRegister, /Get-Date/, "IME register should create a fresh stagin
|
||||
assert.match(imeRegister, /\$PID/, "IME register should include the process id in the staging output to avoid path reuse");
|
||||
assert.match(imeRegister, /-OutputDirectory/, "IME register should pass a staging output directory to the build script");
|
||||
assert.match(imeRegister, /-IntermediateDirectory/, "IME register should pass a staging intermediate directory to the build script");
|
||||
assert.match(imeRegister, /active-registration\.json/, "IME register should persist the staged DLL paths it registered");
|
||||
assert.match(imeUnregister, /active-registration\.json/, "IME unregister should read the registered staged DLL manifest");
|
||||
assert.match(imeUnregister, /windows-ime-register/, "IME unregister should target the same staging root used by register");
|
||||
assert.match(imeUnregister, /ConvertFrom-Json/, "IME unregister should parse persisted registered DLL paths");
|
||||
assert.doesNotMatch(imeUnregister, /windows-ime\\\$folder\\\$Configuration\\OpenLessIme\.dll/, "IME unregister must not only derive legacy build-output DLL paths");
|
||||
|
||||
assert.deepEqual(tauriConfig.bundle.windows.wix.fragmentPaths, ["wix/openless-ime.wxs"]);
|
||||
assert.deepEqual(tauriConfig.bundle.windows.wix.componentRefs, [
|
||||
|
||||
@@ -120,7 +120,7 @@ struct Inner {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows_ime: WindowsImeSessionController,
|
||||
#[cfg(target_os = "windows")]
|
||||
prepared_windows_ime_session: Arc<Mutex<Option<PreparedWindowsImeSessionSlot>>>,
|
||||
prepared_windows_ime_session: Arc<Mutex<Vec<PreparedWindowsImeSessionSlot>>>,
|
||||
state: Mutex<SessionState>,
|
||||
asr: Mutex<Option<SessionResource<ActiveAsr>>>,
|
||||
recorder: Mutex<Option<SessionResource<Recorder>>>,
|
||||
@@ -215,7 +215,7 @@ impl Coordinator {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows_ime: WindowsImeSessionController::new(),
|
||||
#[cfg(target_os = "windows")]
|
||||
prepared_windows_ime_session: Arc::new(Mutex::new(None)),
|
||||
prepared_windows_ime_session: Arc::new(Mutex::new(Vec::new())),
|
||||
state: Mutex::new(SessionState::default()),
|
||||
asr: Mutex::new(None),
|
||||
recorder: Mutex::new(None),
|
||||
@@ -957,10 +957,8 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let prepared = inner.windows_ime.prepare_session();
|
||||
*inner.prepared_windows_ime_session.lock() = Some(PreparedWindowsImeSessionSlot {
|
||||
session_id: current_session_id,
|
||||
prepared,
|
||||
});
|
||||
let mut slots = inner.prepared_windows_ime_session.lock();
|
||||
store_prepared_windows_ime_session(&mut slots, current_session_id, prepared);
|
||||
}
|
||||
// 翻译模式标志重置;hotkey 监听器在 Shift down 时再 set true。
|
||||
inner
|
||||
@@ -1652,19 +1650,28 @@ fn cancel_session(inner: &Arc<Inner>) {
|
||||
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn store_prepared_windows_ime_session(
|
||||
slots: &mut Vec<PreparedWindowsImeSessionSlot>,
|
||||
session_id: u64,
|
||||
prepared: PreparedWindowsImeSession,
|
||||
) {
|
||||
slots.retain(|slot| slot.session_id != session_id);
|
||||
slots.push(PreparedWindowsImeSessionSlot {
|
||||
session_id,
|
||||
prepared,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn take_matching_prepared_windows_ime_session(
|
||||
slot: &mut Option<PreparedWindowsImeSessionSlot>,
|
||||
slots: &mut Vec<PreparedWindowsImeSessionSlot>,
|
||||
session_id: u64,
|
||||
) -> Option<PreparedWindowsImeSession> {
|
||||
if slot
|
||||
.as_ref()
|
||||
.map(|slot| slot.session_id == session_id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return slot.take().map(|slot| slot.prepared);
|
||||
}
|
||||
None
|
||||
let index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.session_id == session_id)?;
|
||||
Some(slots.remove(index).prepared)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -2622,16 +2629,38 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn prepared_windows_ime_slot_is_taken_only_for_matching_session() {
|
||||
let mut slot = Some(PreparedWindowsImeSessionSlot {
|
||||
let mut slots = vec![PreparedWindowsImeSessionSlot {
|
||||
session_id: 2,
|
||||
prepared: PreparedWindowsImeSession::unavailable(),
|
||||
});
|
||||
}];
|
||||
|
||||
assert!(take_matching_prepared_windows_ime_session(&mut slot, 1).is_none());
|
||||
assert_eq!(slot.as_ref().map(|slot| slot.session_id), Some(2));
|
||||
assert!(take_matching_prepared_windows_ime_session(&mut slots, 1).is_none());
|
||||
assert_eq!(
|
||||
slots.iter().map(|slot| slot.session_id).collect::<Vec<_>>(),
|
||||
vec![2]
|
||||
);
|
||||
|
||||
assert!(take_matching_prepared_windows_ime_session(&mut slot, 2).is_some());
|
||||
assert!(slot.is_none());
|
||||
assert!(take_matching_prepared_windows_ime_session(&mut slots, 2).is_some());
|
||||
assert!(slots.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn prepared_windows_ime_sessions_keep_overlapping_snapshots() {
|
||||
let mut slots = Vec::new();
|
||||
store_prepared_windows_ime_session(&mut slots, 1, PreparedWindowsImeSession::unavailable());
|
||||
store_prepared_windows_ime_session(&mut slots, 2, PreparedWindowsImeSession::unavailable());
|
||||
|
||||
assert_eq!(
|
||||
slots.iter().map(|slot| slot.session_id).collect::<Vec<_>>(),
|
||||
vec![1, 2]
|
||||
);
|
||||
|
||||
assert!(take_matching_prepared_windows_ime_session(&mut slots, 1).is_some());
|
||||
assert_eq!(
|
||||
slots.iter().map(|slot| slot.session_id).collect::<Vec<_>>(),
|
||||
vec![2]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -184,7 +184,8 @@ mod windows_impl {
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use windows::core::GUID;
|
||||
use windows::core::{GUID, HRESULT};
|
||||
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
|
||||
COINIT_APARTMENTTHREADED,
|
||||
@@ -209,21 +210,47 @@ mod windows_impl {
|
||||
TF_IPPMF_FORSESSION | TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE | TF_IPPMF_ENABLEPROFILE;
|
||||
const PROFILE_RESTORE_FLAGS: u32 = TF_IPPMF_FORSESSION | TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE;
|
||||
|
||||
struct ComApartment;
|
||||
pub(super) struct ComInitializeOwnership {
|
||||
pub(super) should_uninitialize: bool,
|
||||
}
|
||||
|
||||
pub(super) fn coinitialize_result_ownership(
|
||||
result: HRESULT,
|
||||
) -> WindowsImeProfileResult<ComInitializeOwnership> {
|
||||
if result == RPC_E_CHANGED_MODE {
|
||||
return Ok(ComInitializeOwnership {
|
||||
should_uninitialize: false,
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
.ok()
|
||||
.map(|_| ComInitializeOwnership {
|
||||
should_uninitialize: true,
|
||||
})
|
||||
.map_err(|err| WindowsImeProfileError::WindowsApi(format!("CoInitializeEx: {err}")))
|
||||
}
|
||||
|
||||
struct ComApartment {
|
||||
should_uninitialize: bool,
|
||||
}
|
||||
|
||||
impl ComApartment {
|
||||
fn initialize() -> WindowsImeProfileResult<Self> {
|
||||
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }
|
||||
.ok()
|
||||
.map_err(|err| {
|
||||
WindowsImeProfileError::WindowsApi(format!("CoInitializeEx: {err}"))
|
||||
})?;
|
||||
Ok(Self)
|
||||
let ownership = coinitialize_result_ownership(unsafe {
|
||||
CoInitializeEx(None, COINIT_APARTMENTTHREADED)
|
||||
})?;
|
||||
Ok(Self {
|
||||
should_uninitialize: ownership.should_uninitialize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ComApartment {
|
||||
fn drop(&mut self) {
|
||||
if !self.should_uninitialize {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
CoUninitialize();
|
||||
}
|
||||
@@ -628,6 +655,7 @@ mod windows_tests {
|
||||
use super::*;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::HKL;
|
||||
use windows::Win32::UI::TextServices::GUID_TFCAT_TIP_KEYBOARD;
|
||||
|
||||
@@ -683,4 +711,11 @@ mod windows_tests {
|
||||
assert_eq!(formatted, "{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}");
|
||||
assert!(parse_guid(&formatted).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn com_changed_mode_is_accepted_without_uninitializing() {
|
||||
let ownership = windows_impl::coinitialize_result_ownership(RPC_E_CHANGED_MODE).unwrap();
|
||||
|
||||
assert!(!ownership.should_uninitialize);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user