fix: address Windows IME review cleanup

This commit is contained in:
millionart committed 2026-05-04 00:14:56 +08:00
1 parent 8cb289ea8c
commit f533f6ea05
2 files changed
+162 -57

No files matched your search

+134 -55
View File
@@ -56,6 +56,24 @@ enum ActiveAsr {
Whisper(Arc<WhisperBatchASR>),
}
struct SessionResource<T> {
session_id: u64,
resource: T,
}
impl<T> SessionResource<T> {
fn new(session_id: u64, resource: T) -> Self {
Self {
session_id,
resource,
}
}
fn into_inner(self) -> T {
self.resource
}
}
struct SessionState {
phase: SessionPhase,
started_at: Instant,
@@ -104,8 +122,8 @@ struct Inner {
#[cfg(target_os = "windows")]
prepared_windows_ime_session: Arc<Mutex<Option<PreparedWindowsImeSessionSlot>>>,
state: Mutex<SessionState>,
asr: Mutex<Option<ActiveAsr>>,
recorder: Mutex<Option<Recorder>>,
asr: Mutex<Option<SessionResource<ActiveAsr>>>,
recorder: Mutex<Option<SessionResource<Recorder>>>,
hotkey: Mutex<Option<HotkeyMonitor>>,
hotkey_status: Mutex<HotkeyStatus>,
hotkey_trigger_held: AtomicBool,
@@ -755,15 +773,72 @@ fn request_stop_during_starting(inner: &Arc<Inner>, reason: &str) {
stop_recorder_if_pending_start_stop(inner);
}
fn take_session_resource<T>(slot: &mut Option<SessionResource<T>>, session_id: u64) -> Option<T> {
if slot
.as_ref()
.map(|resource| resource.session_id == session_id)
.unwrap_or(false)
{
slot.take().map(SessionResource::into_inner)
} else {
None
}
}
fn store_asr_for_session(inner: &Arc<Inner>, session_id: u64, asr: ActiveAsr) {
*inner.asr.lock() = Some(SessionResource::new(session_id, asr));
}
fn take_asr_for_session(inner: &Arc<Inner>, session_id: u64) -> Option<ActiveAsr> {
let mut slot = inner.asr.lock();
take_session_resource(&mut slot, session_id)
}
fn cancel_active_asr(asr: ActiveAsr) {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
fn cancel_asr_for_session(inner: &Arc<Inner>, session_id: u64) {
if let Some(asr) = take_asr_for_session(inner, session_id) {
cancel_active_asr(asr);
}
}
fn store_recorder_for_session(inner: &Arc<Inner>, session_id: u64, recorder: Recorder) {
*inner.recorder.lock() = Some(SessionResource::new(session_id, recorder));
}
fn take_recorder_for_session(inner: &Arc<Inner>, session_id: u64) -> Option<Recorder> {
let mut slot = inner.recorder.lock();
take_session_resource(&mut slot, session_id)
}
fn stop_recorder_for_session(inner: &Arc<Inner>, session_id: u64) {
if let Some(recorder) = take_recorder_for_session(inner, session_id) {
recorder.stop();
}
}
fn discard_startup_resources_for_session(inner: &Arc<Inner>, session_id: u64) {
stop_recorder_for_session(inner, session_id);
cancel_asr_for_session(inner, session_id);
}
fn stop_recorder_if_pending_start_stop(inner: &Arc<Inner>) {
let should_stop = {
let (should_stop, session_id) = {
let state = inner.state.lock();
state.phase == SessionPhase::Starting && state.pending_stop
(
state.phase == SessionPhase::Starting && state.pending_stop,
state.session_id,
)
};
if !should_stop {
return;
}
if let Some(rec) = inner.recorder.lock().take() {
if let Some(rec) = take_recorder_for_session(inner, session_id) {
rec.stop();
let elapsed = inner.state.lock().started_at.elapsed().as_millis() as u64;
emit_capsule(inner, CapsuleState::Transcribing, 0.0, elapsed, None, None);
@@ -938,7 +1013,11 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
if active_asr == "whisper" {
let (api_key, base_url, model) = read_whisper_credentials();
let whisper = Arc::new(WhisperBatchASR::new(api_key, base_url, model));
*inner.asr.lock() = Some(ActiveAsr::Whisper(Arc::clone(&whisper)));
store_asr_for_session(
inner,
current_session_id,
ActiveAsr::Whisper(Arc::clone(&whisper)),
);
let consumer: Arc<dyn crate::recorder::AudioConsumer> = whisper;
start_recorder_and_enter_listening(inner, current_session_id, &active_asr, consumer)
.await?;
@@ -948,7 +1027,11 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
let asr = Arc::new(VolcengineStreamingASR::new(creds, hotwords));
let bridge = Arc::new(DeferredAsrBridge::new());
let consumer: Arc<dyn crate::recorder::AudioConsumer> = bridge.clone();
*inner.asr.lock() = Some(ActiveAsr::Volcengine(Arc::clone(&asr)));
store_asr_for_session(
inner,
current_session_id,
ActiveAsr::Volcengine(Arc::clone(&asr)),
);
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer)?;
if let Err(e) = asr.open_session().await {
@@ -959,26 +1042,20 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
"[coord] stale ASR open_session error from session {current_session_id} — ignoring"
);
asr.cancel();
discard_startup_resources_for_session(inner, current_session_id);
restore_prepared_windows_ime_session(inner, current_session_id);
return Ok(());
}
StartupRaceStatus::CancelRaced => {
asr.cancel();
discard_startup_resources_for_session(inner, current_session_id);
restore_prepared_windows_ime_session(inner, current_session_id);
set_phase_idle_if_session_matches(inner, current_session_id);
return Ok(());
}
StartupRaceStatus::ActiveStarting => {}
}
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
discard_startup_resources_for_session(inner, current_session_id);
emit_capsule(
inner,
CapsuleState::Error,
@@ -1000,9 +1077,7 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
StartupRaceStatus::CancelRaced => {
log::info!("[coord] cancel raced during ASR open_session — aborting begin");
asr.cancel();
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
discard_startup_resources_for_session(inner, current_session_id);
restore_prepared_windows_ime_session(inner, current_session_id);
set_phase_idle_if_session_matches(inner, current_session_id);
return Ok(());
@@ -1012,6 +1087,7 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
"[coord] stale ASR open_session continuation from session {current_session_id} — ignoring"
);
asr.cancel();
discard_startup_resources_for_session(inner, current_session_id);
restore_prepared_windows_ime_session(inner, current_session_id);
return Ok(());
}
@@ -1070,19 +1146,14 @@ fn start_recorder_for_starting(
match Recorder::start(consumer, level_handler) {
Ok((rec, runtime_errors)) => {
*inner.recorder.lock() = Some(rec);
store_recorder_for_session(inner, session_id, rec);
spawn_recorder_error_monitor(inner, runtime_errors);
stop_recorder_if_pending_start_stop(inner);
log::info!("[coord] recorder started (asr={active_asr}, phase=Starting)");
}
Err(e) => {
log::error!("[coord] recorder start failed: {e}");
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
cancel_asr_for_session(inner, session_id);
emit_capsule(
inner,
CapsuleState::Error,
@@ -1173,15 +1244,7 @@ fn abort_recording_with_error(inner: &Arc<Inner>, message: String) {
)
};
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
discard_startup_resources_for_session(inner, session_id);
restore_prepared_windows_ime_session(inner, session_id);
emit_capsule(
@@ -1231,19 +1294,12 @@ async fn finish_starting_session(inner: &Arc<Inner>, session_id: u64) {
log::info!(
"[coord] stale recorder/ASR startup continuation from session {session_id} — ignoring"
);
discard_startup_resources_for_session(inner, session_id);
restore_prepared_windows_ime_session(inner, session_id);
}
BeginOutcome::CancelRaced => {
log::info!("[coord] cancel raced during recorder/ASR startup — aborting begin");
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
discard_startup_resources_for_session(inner, session_id);
restore_prepared_windows_ime_session(inner, session_id);
set_phase_idle_if_session_matches(inner, session_id);
}
@@ -1270,11 +1326,11 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
let elapsed = inner.state.lock().started_at.elapsed().as_millis() as u64;
emit_capsule(inner, CapsuleState::Transcribing, 0.0, elapsed, None, None);
if let Some(rec) = inner.recorder.lock().take() {
if let Some(rec) = take_recorder_for_session(inner, current_session_id) {
rec.stop();
}
let asr_opt = inner.asr.lock().take();
let asr_opt = take_asr_for_session(inner, current_session_id);
let asr = match asr_opt {
Some(a) => a,
None => {
@@ -1581,15 +1637,8 @@ fn cancel_session(inner: &Arc<Inner>) {
(phase, state.session_id)
};
if let Some(rec) = inner.recorder.lock().take() {
rec.stop();
}
if let Some(asr) = inner.asr.lock().take() {
match asr {
ActiveAsr::Volcengine(v) => v.cancel(),
ActiveAsr::Whisper(w) => w.cancel(),
}
}
stop_recorder_for_session(inner, session_id);
cancel_asr_for_session(inner, session_id);
restore_prepared_windows_ime_session(inner, session_id);
// Processing 阶段保持 phase=Processing 让 end_session 自己走完检查 + 收尾;
// 其他阶段直接转 Idle。
@@ -2622,6 +2671,36 @@ mod tests {
StartupRaceStatus::StaleContinuation
);
}
#[test]
fn stale_startup_cleanup_keeps_newer_asr_resource() {
let coordinator = Coordinator::new();
let newer_asr = Arc::new(WhisperBatchASR::new(
"key".to_string(),
"http://localhost".to_string(),
"model".to_string(),
));
*coordinator.inner.asr.lock() = Some(SessionResource::new(
2,
ActiveAsr::Whisper(Arc::clone(&newer_asr)),
));
discard_startup_resources_for_session(&coordinator.inner, 1);
assert_eq!(
coordinator
.inner
.asr
.lock()
.as_ref()
.map(|resource| resource.session_id),
Some(2)
);
discard_startup_resources_for_session(&coordinator.inner, 2);
assert!(coordinator.inner.asr.lock().is_none());
}
}
fn enabled_phrases(inner: &Arc<Inner>) -> Vec<String> {
@@ -243,14 +243,31 @@ mod windows_impl {
if profile.dwProfileType == TF_PROFILETYPE_INPUTPROCESSOR {
Ok(ImeProfileSnapshot::text_service(
profile.langid,
format!("{:?}", profile.clsid),
format!("{:?}", profile.guidProfile),
guid_to_braced_string(profile.clsid),
guid_to_braced_string(profile.guidProfile),
))
} else {
keyboard_layout_snapshot_from_tsf(profile.langid, profile.hkl)
}
}
pub(super) fn guid_to_braced_string(guid: GUID) -> String {
format!(
"{{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
guid.data1,
guid.data2,
guid.data3,
guid.data4[0],
guid.data4[1],
guid.data4[2],
guid.data4[3],
guid.data4[4],
guid.data4[5],
guid.data4[6],
guid.data4[7],
)
}
pub fn activate_openless_profile() -> WindowsImeProfileResult<()> {
let clsid = parse_guid(OPENLESS_TEXT_SERVICE_CLSID_BRACED)?;
let profile_guid = parse_guid(OPENLESS_PROFILE_GUID_BRACED)?;
@@ -657,4 +674,13 @@ mod windows_tests {
.to_string()
.contains("active keyboard layout profile has no HKL"));
}
#[test]
fn guid_snapshot_strings_are_canonical_and_parseable() {
let guid = windows::core::GUID::from_u128(0x6b9f3f4f_5ee7_42d6_9c61_9f80b03a5d7d);
let formatted = windows_impl::guid_to_braced_string(guid);
assert_eq!(formatted, "{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}");
assert!(parse_guid(&formatted).is_ok());
}
}