feat(diagnostic): 阶段 0A 输入链路被动探测与线程归属复现

This commit is contained in:
Codex
2026-10-05 01:10:02 +08:00
parent c0eca1d4bd
commit c218c25310
12 changed files with 2417 additions and 11 deletions
+9 -2
View File
@@ -29,6 +29,8 @@
<!-- Normal and sanitizer outputs use separate directories so ASan builds stay isolated. --> <!-- Normal and sanitizer outputs use separate directories so ASan builds stay isolated. -->
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir> <OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir> <IntDir>$(SolutionDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
<!-- Input probing is diagnostic-only and must be explicitly enabled at build time. -->
<StellarXInputProbe Condition="'$(StellarXInputProbe)'==''">false</StellarXInputProbe>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(EnableAsan)'=='true' And '$(Platform)'=='x64'"> <PropertyGroup Condition="'$(EnableAsan)'=='true' And '$(Platform)'=='x64'">
<EnableASAN>true</EnableASAN> <EnableASAN>true</EnableASAN>
@@ -45,18 +47,23 @@
<AdditionalIncludeDirectories>$(ProjectDir)include\StellarX;$(ProjectDir)src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>$(ProjectDir)include\StellarX;$(ProjectDir)src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(StellarXInputProbe)'=='true'">
<ClCompile>
<PreprocessorDefinitions>STELLARX_ENABLE_INPUT_PROBE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<!-- Public and internal headers are kept in this project so all consumers use one implementation. --> <!-- Public and internal headers are kept in this project so all consumers use one implementation. -->
<ItemGroup> <ItemGroup>
<ClInclude Include="include\StellarX\Button.h" /><ClInclude Include="include\StellarX\Canvas.h" /><ClInclude Include="include\StellarX\Control.h" /> <ClInclude Include="include\StellarX\Button.h" /><ClInclude Include="include\StellarX\Canvas.h" /><ClInclude Include="include\StellarX\Control.h" />
<ClInclude Include="include\StellarX\CoreTypes.h" /><ClInclude Include="include\StellarX\Dialog.h" /><ClInclude Include="include\StellarX\Label.h" /> <ClInclude Include="include\StellarX\CoreTypes.h" /><ClInclude Include="include\StellarX\Dialog.h" /><ClInclude Include="include\StellarX\Label.h" />
<ClInclude Include="include\StellarX\MessageBox.h" /><ClInclude Include="include\StellarX\StellarX.h" /><ClInclude Include="src\StellarX\internal\SxEventLoopInternal.h" /><ClInclude Include="include\StellarX\SxLog.h" /><ClInclude Include="src\StellarX\internal\SxTextInternal.h" /> <ClInclude Include="include\StellarX\MessageBox.h" /><ClInclude Include="include\StellarX\StellarX.h" /><ClInclude Include="src\StellarX\internal\SxEventLoopInternal.h" /><ClInclude Include="src\StellarX\internal\SxInputProbeInternal.h" /><ClInclude Include="include\StellarX\SxLog.h" /><ClInclude Include="src\StellarX\internal\SxTextInternal.h" />
<ClInclude Include="include\StellarX\TabControl.h" /><ClInclude Include="include\StellarX\Table.h" /><ClInclude Include="include\StellarX\TextBox.h" /><ClInclude Include="include\StellarX\Window.h" /> <ClInclude Include="include\StellarX\TabControl.h" /><ClInclude Include="include\StellarX\Table.h" /><ClInclude Include="include\StellarX\TextBox.h" /><ClInclude Include="include\StellarX\Window.h" />
</ItemGroup> </ItemGroup>
<!-- Framework source files compiled into StellarX.Core.lib. --> <!-- Framework source files compiled into StellarX.Core.lib. -->
<ItemGroup> <ItemGroup>
<ClCompile Include="src\StellarX\Button.cpp" /><ClCompile Include="src\StellarX\Canvas.cpp" /><ClCompile Include="src\StellarX\Control.cpp" /> <ClCompile Include="src\StellarX\Button.cpp" /><ClCompile Include="src\StellarX\Canvas.cpp" /><ClCompile Include="src\StellarX\Control.cpp" />
<ClCompile Include="src\StellarX\Dialog.cpp" /><ClCompile Include="src\StellarX\Label.cpp" /><ClCompile Include="src\StellarX\MessageBox.cpp" /> <ClCompile Include="src\StellarX\Dialog.cpp" /><ClCompile Include="src\StellarX\Label.cpp" /><ClCompile Include="src\StellarX\MessageBox.cpp" />
<ClCompile Include="src\StellarX\SxLog.cpp" /><ClCompile Include="src\StellarX\TabControl.cpp" /><ClCompile Include="src\StellarX\Table.cpp" /> <ClCompile Include="src\StellarX\SxInputProbe.cpp" /><ClCompile Include="src\StellarX\SxLog.cpp" /><ClCompile Include="src\StellarX\TabControl.cpp" /><ClCompile Include="src\StellarX\Table.cpp" />
<ClCompile Include="src\StellarX\TextBox.cpp" /><ClCompile Include="src\StellarX\Window.cpp" /> <ClCompile Include="src\StellarX\TextBox.cpp" /><ClCompile Include="src\StellarX\Window.cpp" />
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+10
View File
@@ -11,6 +11,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.Tests", "tests\Ste
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.UiFixture", "tests\StellarX.UiFixture.vcxproj", "{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.UiFixture", "tests\StellarX.UiFixture.vcxproj", "{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EasyX.ThreadProbe", "tests\EasyX.ThreadProbe.vcxproj", "{6D338118-A536-4E4F-A4F5-3C73C5CA107A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64 Debug|x64 = Debug|x64
@@ -51,6 +53,14 @@ Global
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x64.Build.0 = Release|x64 {D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x64.Build.0 = Release|x64
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.ActiveCfg = Release|Win32 {D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.ActiveCfg = Release|Win32
{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.Build.0 = Release|Win32 {D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}.Release|x86.Build.0 = Release|Win32
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Debug|x64.ActiveCfg = Debug|x64
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Debug|x64.Build.0 = Debug|x64
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Debug|x86.ActiveCfg = Debug|Win32
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Debug|x86.Build.0 = Debug|Win32
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Release|x64.ActiveCfg = Release|x64
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Release|x64.Build.0 = Release|x64
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Release|x86.ActiveCfg = Release|Win32
{6D338118-A536-4E4F-A4F5-3C73C5CA107A}.Release|x86.Build.0 = Release|Win32
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+77
View File
@@ -0,0 +1,77 @@
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$ErrorActionPreference = 'Stop'
$resolved = Resolve-Path -LiteralPath $Path
$item = Get-Item -LiteralPath $resolved
$file = if ($item.PSIsContainer) {
Get-ChildItem -LiteralPath $item.FullName -Filter '*.jsonl' -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
}
else {
$item
}
if (-not $file) { throw "No JSONL file found: $Path" }
$records = New-Object System.Collections.Generic.List[object]
$lineNumber = 0
Get-Content -LiteralPath $file.FullName | ForEach-Object {
$lineNumber++
try { $records.Add(($_ | ConvertFrom-Json)) }
catch { throw "Invalid JSONL at line $lineNumber in $($file.FullName): $($_.Exception.Message)" }
}
$sequenceStrict = $true
for ($index = 1; $index -lt $records.Count; $index++) {
if ([uint64]$records[$index].sequence -le [uint64]$records[$index - 1].sequence) {
$sequenceStrict = $false
break
}
}
$immQueries = @($records | Where-Object recordType -eq 'imm_query')
$successfulResults = @($immQueries | Where-Object immQueryStatus -eq 'success')
$echoSummaries = @($records | Where-Object recordType -eq 'echo_correlation')
$textLeaks = @($records | Where-Object {
$_.messageCategory -eq 'text' -and $_.wParamHex -ne $null
})
$lost = @($records | Where-Object recordType -eq 'records_lost')
$attach = $records | Where-Object recordType -eq 'window_attach' | Select-Object -First 1
$correlationCounts = [ordered]@{}
foreach ($entry in $echoSummaries) {
$key = "$($entry.echoChannel):$($entry.correlationStatus)"
if (-not $correlationCounts.Contains($key)) { $correlationCounts[$key] = 0 }
$correlationCounts[$key]++
}
[pscustomobject]@{
file = $file.FullName
recordCount = $records.Count
sequenceStrict = $sequenceStrict
recordsLost = $lost.Count
textRawWParamLeaks = $textLeaks.Count
isWindowUnicode = $attach.isWindowUnicode
ansiCodePage = $attach.ansiCodePage
wndProcThreadIds = @($records | Where-Object sourceBackend -eq 'Win32WndProc' |
Select-Object -ExpandProperty threadId -Unique)
hostThreadIds = @($records | Where-Object sourceBackend -eq 'HostEventLoop' |
Select-Object -ExpandProperty threadId -Unique)
imeStartCount = @($records | Where-Object messageName -eq 'WM_IME_STARTCOMPOSITION').Count
imeCompositionCount = @($records | Where-Object messageName -eq 'WM_IME_COMPOSITION').Count
imeEndCount = @($records | Where-Object messageName -eq 'WM_IME_ENDCOMPOSITION').Count
inputLanguageChangeRequestCount = @($records |
Where-Object messageName -eq 'WM_INPUTLANGCHANGEREQUEST').Count
inputLanguageChangeCount = @($records | Where-Object messageName -eq 'WM_INPUTLANGCHANGE').Count
immQueryCount = $immQueries.Count
successfulResultCount = $successfulResults.Count
successfulUtf16Lengths = @($successfulResults | Select-Object -ExpandProperty utf16Length)
successfulScalarCounts = @($successfulResults | Select-Object -ExpandProperty scalarCount)
digestPresentCount = @($successfulResults | Where-Object sessionDigest -ne $null).Count
correlationCounts = [pscustomobject]$correlationCounts
closeObserved = @($records | Where-Object messageName -eq 'WM_CLOSE').Count -gt 0
sessionEndObserved = @($records | Where-Object recordType -eq 'session_end').Count -gt 0
}
+33
View File
@@ -0,0 +1,33 @@
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Debug',
[ValidateSet('x64', 'Win32')]
[string]$Platform = 'x64'
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$probe = Join-Path $root "build\$Platform\$Configuration\EasyX.ThreadProbe.exe"
if (-not (Test-Path -LiteralPath $probe)) {
throw "Thread probe not built: $probe"
}
$resultRoot = Join-Path $root 'TestResults\easyx-thread-probe'
New-Item -ItemType Directory -Force -Path $resultRoot | Out-Null
$resultPath = Join-Path $resultRoot ((Get-Date -Format 'yyyyMMdd-HHmmss-fff') + "-$PID.json")
& $probe $resultPath
if ($LASTEXITCODE -ne 0) {
throw "EasyX thread probe failed with exit code $LASTEXITCODE"
}
if (-not (Test-Path -LiteralPath $resultPath)) {
throw "Thread probe result not found: $resultPath"
}
$result = Get-Content -Raw -LiteralPath $resultPath | ConvertFrom-Json
if (-not $result.completed) {
throw "Thread probe did not complete: $resultPath"
}
$result | Add-Member -NotePropertyName resultPath -NotePropertyValue $resultPath
$result
+76
View File
@@ -0,0 +1,76 @@
param(
[Parameter(Mandatory = $true)]
[ValidateSet('MicrosoftPinyin', 'Sogou')]
[string]$ImeName,
[switch]$InputLanguageOnly
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$msbuild = if ($env:STELLARX_MSBUILD) { $env:STELLARX_MSBUILD } else { 'D:\vs2022\MSBuild\Current\Bin\MSBuild.exe' }
$project = Join-Path $root 'tests\StellarX.UiFixture.vcxproj'
$fixture = Join-Path $root 'build\x64\Debug\StellarX.UiFixture.exe'
if (-not (Test-Path -LiteralPath $msbuild)) { throw "MSBuild not found: $msbuild" }
$pathKeyCount = @(
[Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::Process).Keys |
Where-Object { [string]$_ -ieq 'Path' }
).Count
if ($pathKeyCount -gt 1) {
# Codex 等宿主可能注入同时存在的 PATH/Path;MSBuild 启动 CL 时会因重复键失败。
# cmd 的 set 精确删除第一份 PATH 后仍保留 Path,只影响这个构建子进程。
$buildCommand = 'set "PATH=" & "' + $msbuild + '" "' + $project +
'" /m /t:Build /p:Configuration=Debug /p:Platform=x64 /p:StellarXInputProbe=true /v:minimal'
& $env:ComSpec /d /c $buildCommand
}
else {
& $msbuild $project /m /t:Build /p:Configuration=Debug /p:Platform=x64 /p:StellarXInputProbe=true /v:minimal
}
if ($LASTEXITCODE -ne 0) { throw 'Input probe fixture build failed' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
$runDir = Join-Path $root "TestResults\InputProbe\manual-$stamp-$ImeName"
$fixtureDir = Join-Path $runDir 'fixture'
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
New-Item -ItemType Directory -Force -Path $fixtureDir | Out-Null
Write-Host "Manual IME probe: $ImeName"
if ($InputLanguageOnly) {
Write-Host 'Input-language-only checklist:'
Write-Host ' 1. Click the EasyX window so it owns keyboard focus'
Write-Host ' 2. Use Win+Space to select a different language/keyboard layout, for example English (US)'
Write-Host ' Switching only between two Chinese IMEs stays in the same HKL and may not emit request/change'
Write-Host ' 3. Type A, then use Win+Space to return to the original input method/layout'
}
else {
Write-Host 'Use only these non-sensitive strings:'
Write-Host ' 1. zhongwentest -> choose a Chinese candidate and commit'
Write-Host ' 2. haha -> commit the same Chinese character twice'
Write-Host ' 3. ceshi -> press Escape before committing'
Write-Host ' 4. Type a candidate sequence and select it with a number key'
Write-Host ' 5. Use Win+Space to select a different language/keyboard layout (for example English (US)), then type A'
}
Write-Host 'Close the EasyX window when finished. The script will then print a privacy-safe summary.'
$launchCommand = 'set "STELLARX_INPUT_PROBE=1" & set "STELLARX_INPUT_PROBE_DIR=' + $runDir +
'" & "' + $fixture + '" input-probe "' + $fixtureDir + '"'
& $env:ComSpec /d /c $launchCommand
$exitCode = $LASTEXITCODE
$summary = & (Join-Path $PSScriptRoot 'analyze-input-probe.ps1') -Path $runDir
# EX_SHOWCONSOLE 下由脚本托管的 EasyX 进程在原生标题栏关闭时,部分控制台宿主会把
# 正常窗口关闭映射为 STATUS_CONTROL_C_EXIT。必须先验证探测日志完整性和关闭证据,
# 不能无条件忽略这个非零退出码,否则真实崩溃也可能被误报为成功。
$controlCExit = -1073741510 # 0xC000013A
$validNativeClose = $summary.sequenceStrict -and
$summary.closeObserved -and
$summary.textRawWParamLeaks -eq 0
$summary | Format-List
Write-Host "Result directory: $runDir"
if ($exitCode -ne 0 -and -not ($exitCode -eq $controlCExit -and $validNativeClose)) {
throw "Input probe fixture exited with code $exitCode"
}
if ($exitCode -eq $controlCExit) {
Write-Host 'Fixture exit code: 0xC000013A (accepted after validating native-close probe records).'
}
+10
View File
@@ -1,6 +1,7 @@
#include "Dialog.h" #include "Dialog.h"
#include "SxLog.h" #include "SxLog.h"
#include "SxEventLoopInternal.h" #include "SxEventLoopInternal.h"
#include "SxInputProbeInternal.h"
Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal) Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal)
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text) : Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
@@ -187,6 +188,7 @@ void Dialog::Show()
{ {
show = false; show = false;
hWnd.dialogOpen = false; hWnd.dialogOpen = false;
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
return; return;
} }
int lastW = rc0.right - rc0.left; int lastW = rc0.right - rc0.left;
@@ -209,6 +211,7 @@ void Dialog::Show()
{ {
show = false; show = false;
hWnd.dialogOpen = false; hWnd.dialogOpen = false;
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
break; break;
} }
const int cw = rc.right - rc.left; const int cw = rc.right - rc.left;
@@ -232,17 +235,22 @@ void Dialog::Show()
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY) // ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY)
ExMessage msg; ExMessage msg;
auto probeBatch = StellarX::Internal::inputProbeHostBatchBegin(hWnd.getHwnd(), true);
const auto batch = StellarX::Internal::drainMessageBatch(msg, const auto batch = StellarX::Internal::drainMessageBatch(msg,
[](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY); }, [](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY); },
[&](const ExMessage& next) { [&](const ExMessage& next) {
StellarX::Internal::inputProbeObserveEasyXMessage(hWnd.getHwnd(), probeBatch, next);
handleEvent(next); handleEvent(next);
return show; return show;
}); });
processedMessages = static_cast<int>(batch.processed); processedMessages = static_cast<int>(batch.processed);
StellarX::Internal::inputProbeHostBatchEnd(
hWnd.getHwnd(), probeBatch, batch.processed, batch.stopped);
if (!hWnd.hasLiveNativeWindow()) if (!hWnd.hasLiveNativeWindow())
{ {
show = false; show = false;
hWnd.dialogOpen = false; hWnd.dialogOpen = false;
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
break; break;
} }
@@ -254,6 +262,8 @@ void Dialog::Show()
EndBatchDraw(); EndBatchDraw();
dirty = false; dirty = false;
} }
if (!hWnd.isMutationTransactionActive())
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
if (batch.shouldSleep()) if (batch.shouldSleep())
Sleep(1); Sleep(1);
File diff suppressed because it is too large Load Diff
+32 -9
View File
@@ -2,6 +2,7 @@
#include "Dialog.h" #include "Dialog.h"
#include"SxLog.h" #include"SxLog.h"
#include "SxEventLoopInternal.h" #include "SxEventLoopInternal.h"
#include "SxInputProbeInternal.h"
#include <easyx.h> #include <easyx.h>
#include <algorithm> #include <algorithm>
// 可能频繁出现且对调试信息干扰较大的消息(例如鼠标移动), // 可能频繁出现且对调试信息干扰较大的消息(例如鼠标移动),
@@ -679,6 +680,7 @@ Window::~Window()
dialogs.clear(); dialogs.clear();
controls.clear(); controls.clear();
background.reset(); background.reset();
StellarX::Internal::inputProbeDetach(hWnd);
if (hWnd && procHooked && oldWndProc && hasLiveNativeWindow()) if (hWnd && procHooked && oldWndProc && hasLiveNativeWindow())
{ {
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc); SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc);
@@ -707,11 +709,12 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
{ {
return DefWindowProc(h, m, w, l); return DefWindowProc(h, m, w, l);
} }
const auto probeCall = StellarX::Internal::inputProbeWndProcEnter(h, m, w, l);
// 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁 // 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁
if (m == WM_ERASEBKGND) if (m == WM_ERASEBKGND)
{ {
return 1; return StellarX::Internal::inputProbeWndProcLeave(probeCall, 1, false);
} }
// 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖) // 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖)
@@ -720,7 +723,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
SX_LOGI("Resize") << SX_T("WM_ENTERSIZEMOVE: 开始测量尺寸","WM_ENTERSIZEMOVE: begin sizing"); SX_LOGI("Resize") << SX_T("WM_ENTERSIZEMOVE: 开始测量尺寸","WM_ENTERSIZEMOVE: begin sizing");
self->isSizing = true; self->isSizing = true;
SendMessage(h, WM_SETREDRAW, FALSE, 0); SendMessage(h, WM_SETREDRAW, FALSE, 0);
return 0; return StellarX::Internal::inputProbeWndProcLeave(probeCall, 0, false);
} }
// 关键点③:拉伸中 → 仅执行“最小尺寸夹紧”,不做对齐/节流/回滚,保持系统自然流畅 // 关键点③:拉伸中 → 仅执行“最小尺寸夹紧”,不做对齐/节流/回滚,保持系统自然流畅
@@ -732,7 +735,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
int currentHeight = prc->bottom - prc->top; int currentHeight = prc->bottom - prc->top;
if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000) if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000)
{ {
return TRUE; return StellarX::Internal::inputProbeWndProcLeave(probeCall, TRUE, false);
} }
RECT before = *prc;// 记录调整前矩形以便日志输出 RECT before = *prc;// 记录调整前矩形以便日志输出
@@ -745,7 +748,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
<< SX_T("之前=(","before=(") << (before.right - before.left) << "x" << (before.bottom - before.top) << ") " << SX_T("之前=(","before=(") << (before.right - before.left) << "x" << (before.bottom - before.top) << ") "
<< SX_T("之后=(","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")"; << SX_T("之后=(","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")";
} }
return TRUE; return StellarX::Internal::inputProbeWndProcLeave(probeCall, TRUE, false);
} }
// 关键点④:拉伸结束 → 解冻重绘 + 统一收口(记录最终尺寸 -> 标记 needResizeDirty) // 关键点④:拉伸结束 → 解冻重绘 + 统一收口(记录最终尺寸 -> 标记 needResizeDirty)
@@ -768,7 +771,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
// 立即解冻重绘标志,同时标记区域为有效,避免触发额外 WM_PAINT。 // 立即解冻重绘标志,同时标记区域为有效,避免触发额外 WM_PAINT。
SendMessage(h, WM_SETREDRAW, TRUE, 0); SendMessage(h, WM_SETREDRAW, TRUE, 0);
ValidateRect(h, nullptr); ValidateRect(h, nullptr);
return 0; return StellarX::Internal::inputProbeWndProcLeave(probeCall, 0, false);
} }
// 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合) // 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合)
@@ -782,20 +785,23 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
AdjustWindowRectEx(&rc, style, FALSE, ex); AdjustWindowRectEx(&rc, style, FALSE, ex);
mmi->ptMinTrackSize.x = rc.right - rc.left; mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top; mmi->ptMinTrackSize.y = rc.bottom - rc.top;
return 0; return StellarX::Internal::inputProbeWndProcLeave(probeCall, 0, false);
} }
// 其它消息:回落到旧过程 // 其它消息:回落到旧过程
const LRESULT result = self->oldWndProc const bool forwardedToOldProc = self->oldWndProc != nullptr;
const LRESULT result = forwardedToOldProc
? CallWindowProc(self->oldWndProc, h, m, w, l) ? CallWindowProc(self->oldWndProc, h, m, w, l)
: DefWindowProc(h, m, w, l); : DefWindowProc(h, m, w, l);
const LRESULT observedResult = StellarX::Internal::inputProbeWndProcLeave(
probeCall, result, forwardedToOldProc);
if (m == WM_NCDESTROY) if (m == WM_NCDESTROY)
{ {
// 必须放在旧窗口过程返回之后,并作为本函数最后一次 self 访问。 // 必须放在旧窗口过程返回之后,并作为本函数最后一次 self 访问。
// 宿主线程看到 false 后即可安全离开 runEventLoop 并析构 Window。 // 宿主线程看到 false 后即可安全离开 runEventLoop 并析构 Window。
self->nativeWindowAlive.store(false, std::memory_order_release); self->nativeWindowAlive.store(false, std::memory_order_release);
} }
return result; return observedResult;
} }
// ---------------- 绘制 ---------------- // ---------------- 绘制 ----------------
@@ -823,8 +829,11 @@ void Window::draw()
if (!procHooked) if (!procHooked)
{ {
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this); SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
StellarX::Internal::inputProbeAttach(hWnd);
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk); oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
procHooked = (oldWndProc != nullptr); procHooked = (oldWndProc != nullptr);
if (!procHooked)
StellarX::Internal::inputProbeDetach(hWnd);
} }
if (!headline.empty()) if (!headline.empty())
{ {
@@ -860,8 +869,11 @@ void Window::draw(std::string imagePath)
if (!procHooked) if (!procHooked)
{ {
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this); SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
StellarX::Internal::inputProbeAttach(hWnd);
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk); oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
procHooked = (oldWndProc != nullptr); procHooked = (oldWndProc != nullptr);
if (!procHooked)
StellarX::Internal::inputProbeDetach(hWnd);
} }
bkImageFile = std::move(imagePath); bkImageFile = std::move(imagePath);
@@ -909,12 +921,17 @@ int Window::runEventLoop()
while (true) while (true)
{ {
if (!hasLiveNativeWindow()) if (!hasLiveNativeWindow())
{
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
return 0; return 0;
}
// drainMessageBatch 将一轮消息限制在固定上限;批次结束后统一处理 resize、tick 和重绘。 // drainMessageBatch 将一轮消息限制在固定上限;批次结束后统一处理 resize、tick 和重绘。
auto probeBatch = StellarX::Internal::inputProbeHostBatchBegin(hWnd, false);
const auto batch = StellarX::Internal::drainMessageBatch(msg, const auto batch = StellarX::Internal::drainMessageBatch(msg,
[](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY | EX_WINDOW, true); }, [](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY | EX_WINDOW, true); },
[&](const ExMessage&) { [&](const ExMessage& next) {
StellarX::Internal::inputProbeObserveEasyXMessage(hWnd, probeBatch, next);
bool consume = false; // 每条消息独立判断,不能把上一条消息的消费状态带入下一条。 bool consume = false; // 每条消息独立判断,不能把上一条消息的消费状态带入下一条。
if (msg.message == WM_CLOSE) if (msg.message == WM_CLOSE)
return false; return false;
@@ -1027,8 +1044,12 @@ int Window::runEventLoop()
flushPendingMutationsInTree(); flushPendingMutationsInTree();
return true; return true;
}); });
StellarX::Internal::inputProbeHostBatchEnd(hWnd, probeBatch, batch.processed, batch.stopped);
if (batch.stopped || !hasLiveNativeWindow()) if (batch.stopped || !hasLiveNativeWindow())
{
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
return 0; return 0;
}
const ULONGLONG nowMs = GetTickCount64(); const ULONGLONG nowMs = GetTickCount64();
// tick 独立于鼠标消息推进 Tooltip 等延时状态,仍放在事务内避免回调立即改 vector。 // tick 独立于鼠标消息推进 Tooltip 等延时状态,仍放在事务内避免回调立即改 vector。
@@ -1188,6 +1209,8 @@ int Window::runEventLoop()
} }
sweepClosedDialogs(); sweepClosedDialogs();
if (!isMutationTransactionActive())
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
// 只在真正空闲时短暂让出 CPU;有消息的批次不得人为限速。 // 只在真正空闲时短暂让出 CPU;有消息的批次不得人为限速。
if (batch.shouldSleep()) if (batch.shouldSleep())
@@ -0,0 +1,77 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <windows.h>
struct ExMessage;
namespace StellarX::Internal
{
struct InputProbeWndProcToken
{
std::uint64_t callId = 0;
std::uint64_t enterSequence = 0;
std::uint64_t compositionId = 0;
std::uint32_t callDepth = 0;
UINT message = 0;
bool depthCounted = false;
bool recorded = false;
};
struct InputProbeBatchToken
{
std::uint64_t batchId = 0;
std::uint64_t startedQpcTicks = 0;
std::uint32_t dispatchDepth = 0;
std::uint32_t modalDepth = 0;
bool active = false;
bool modal = false;
bool materialized = false;
};
#if defined(STELLARX_ENABLE_INPUT_PROBE)
void inputProbeAttach(HWND hwnd) noexcept;
void inputProbeDetach(HWND hwnd) noexcept;
InputProbeWndProcToken inputProbeWndProcEnter(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept;
LRESULT inputProbeWndProcLeave(
const InputProbeWndProcToken& token, LRESULT result, bool forwardedToOldProc) noexcept;
InputProbeBatchToken inputProbeHostBatchBegin(HWND hwnd, bool modal) noexcept;
void inputProbeObserveEasyXMessage(
HWND hwnd, InputProbeBatchToken& batch, const ExMessage& message) noexcept;
void inputProbeHostBatchEnd(
HWND hwnd, InputProbeBatchToken& batch, std::size_t processed, bool stopped) noexcept;
void inputProbeFlushAtSafePoint(HWND hwnd) noexcept;
#else
inline void inputProbeAttach(HWND) noexcept {}
inline void inputProbeDetach(HWND) noexcept {}
inline InputProbeWndProcToken inputProbeWndProcEnter(
HWND, UINT, WPARAM, LPARAM) noexcept
{
return {};
}
inline LRESULT inputProbeWndProcLeave(
const InputProbeWndProcToken&, LRESULT result, bool) noexcept
{
return result;
}
inline InputProbeBatchToken inputProbeHostBatchBegin(HWND, bool) noexcept
{
return {};
}
inline void inputProbeObserveEasyXMessage(
HWND, InputProbeBatchToken&, const ExMessage&) noexcept {}
inline void inputProbeHostBatchEnd(
HWND, InputProbeBatchToken&, std::size_t, bool) noexcept {}
inline void inputProbeFlushAtSafePoint(HWND) noexcept {}
#endif
}
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32"><Configuration>Debug</Configuration><Platform>Win32</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32"><Configuration>Release</Configuration><Platform>Win32</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
<ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{6D338118-A536-4E4F-A4F5-3C73C5CA107A}</ProjectGuid>
<RootNamespace>EasyXThreadProbe</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries Condition="'$(Configuration)'=='Debug'">true</UseDebugLibraries>
<UseDebugLibraries Condition="'$(Configuration)'=='Release'">false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)..\build\obj\$(ProjectName)\$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="EasyXThreadProbe.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+137
View File
@@ -0,0 +1,137 @@
#include <easyx.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <fstream>
#include <string>
#include <windows.h>
namespace
{
constexpr UINT ProbeMessage = WM_APP + 0x41;
std::atomic<DWORD> probeWndProcThread{ 0 };
std::atomic<DWORD> keyWndProcThread{ 0 };
std::atomic<unsigned int> keyWndProcCount{ 0 };
HANDLE probeObserved = nullptr;
WNDPROC previousWndProc = nullptr;
LRESULT CALLBACK ProbeWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == ProbeMessage)
{
probeWndProcThread.store(GetCurrentThreadId(), std::memory_order_release);
if (probeObserved)
SetEvent(probeObserved);
}
else if (message == WM_KEYDOWN || message == WM_KEYUP)
{
keyWndProcThread.store(GetCurrentThreadId(), std::memory_order_release);
keyWndProcCount.fetch_add(1, std::memory_order_relaxed);
}
return previousWndProc
? CallWindowProcA(previousWndProc, hwnd, message, wParam, lParam)
: DefWindowProcA(hwnd, message, wParam, lParam);
}
bool postFixedKey(HWND hwnd)
{
constexpr LPARAM KeyDownLParam = 1 | (0x1E << 16);
constexpr LPARAM KeyUpLParam = KeyDownLParam | (1LL << 30) | (1LL << 31);
return PostMessageA(hwnd, WM_KEYDOWN, 'A', KeyDownLParam) != FALSE &&
PostMessageA(hwnd, WM_KEYUP, 'A', KeyUpLParam) != FALSE;
}
}
int main(int argc, char** argv)
{
const std::string outputPath = argc > 1 ? argv[1] : "easyx-thread-probe.json";
const DWORD processId = GetCurrentProcessId();
const DWORD mainThreadBeforeInit = GetCurrentThreadId();
HWND hwnd = initgraph(320, 180, 1);
const DWORD mainThreadAfterInit = GetCurrentThreadId();
DWORD windowProcessId = 0;
const DWORD windowOwnerThread = GetWindowThreadProcessId(hwnd, &windowProcessId);
const bool windowUnicode = IsWindowUnicode(hwnd) != FALSE;
ShowWindow(hwnd, SW_HIDE);
probeObserved = CreateEventW(nullptr, TRUE, FALSE, nullptr);
SetLastError(ERROR_SUCCESS);
previousWndProc = reinterpret_cast<WNDPROC>(
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&ProbeWndProc)));
const DWORD subclassError = previousWndProc ? ERROR_SUCCESS : GetLastError();
bool probePosted = false;
bool probeDelivered = false;
bool keyPosted = false;
DWORD peekCallerThread = 0;
unsigned int easyxKeyCount = 0;
if (previousWndProc && probeObserved)
{
probePosted = PostMessageA(hwnd, ProbeMessage, 0, 0) != FALSE;
probeDelivered = probePosted && WaitForSingleObject(probeObserved, 5000) == WAIT_OBJECT_0;
keyPosted = postFixedKey(hwnd);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (std::chrono::steady_clock::now() < deadline && easyxKeyCount < 2)
{
ExMessage message{};
peekCallerThread = GetCurrentThreadId();
if (peekmessage(&message, EX_KEY, true))
{
if ((message.message == WM_KEYDOWN || message.message == WM_KEYUP) &&
message.vkcode == 'A')
{
++easyxKeyCount;
}
}
else
{
Sleep(1);
}
}
}
const DWORD observedProbeThread = probeWndProcThread.load(std::memory_order_acquire);
const DWORD observedKeyThread = keyWndProcThread.load(std::memory_order_acquire);
const unsigned int observedKeyWndProcCount = keyWndProcCount.load(std::memory_order_relaxed);
const bool separateWindowThread = windowOwnerThread != 0 && windowOwnerThread != mainThreadAfterInit;
const bool wndProcMatchesOwner = observedProbeThread != 0 && observedProbeThread == windowOwnerThread;
const bool keyCrossedBothBackends = keyPosted && observedKeyWndProcCount >= 2 && easyxKeyCount >= 2;
const bool completed = hwnd != nullptr && previousWndProc != nullptr && probeDelivered &&
wndProcMatchesOwner && keyCrossedBothBackends;
{
std::ofstream output(outputPath, std::ios::trunc);
output << "{\n"
<< " \"schemaVersion\": 1,\n"
<< " \"processId\": " << processId << ",\n"
<< " \"windowProcessId\": " << windowProcessId << ",\n"
<< " \"mainThreadBeforeInit\": " << mainThreadBeforeInit << ",\n"
<< " \"mainThreadAfterInit\": " << mainThreadAfterInit << ",\n"
<< " \"windowOwnerThread\": " << windowOwnerThread << ",\n"
<< " \"probeWndProcThread\": " << observedProbeThread << ",\n"
<< " \"keyWndProcThread\": " << observedKeyThread << ",\n"
<< " \"peekCallerThread\": " << peekCallerThread << ",\n"
<< " \"windowUnicode\": " << (windowUnicode ? "true" : "false") << ",\n"
<< " \"subclassError\": " << subclassError << ",\n"
<< " \"probePosted\": " << (probePosted ? "true" : "false") << ",\n"
<< " \"probeDelivered\": " << (probeDelivered ? "true" : "false") << ",\n"
<< " \"keyPosted\": " << (keyPosted ? "true" : "false") << ",\n"
<< " \"keyWndProcCount\": " << observedKeyWndProcCount << ",\n"
<< " \"easyxKeyCount\": " << easyxKeyCount << ",\n"
<< " \"separateWindowThread\": " << (separateWindowThread ? "true" : "false") << ",\n"
<< " \"wndProcMatchesOwner\": " << (wndProcMatchesOwner ? "true" : "false") << ",\n"
<< " \"keyCrossedBothBackends\": " << (keyCrossedBothBackends ? "true" : "false") << ",\n"
<< " \"completed\": " << (completed ? "true" : "false") << "\n"
<< "}\n";
}
if (previousWndProc)
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(previousWndProc));
if (probeObserved)
CloseHandle(probeObserved);
closegraph();
return completed ? 0 : 2;
}
+20
View File
@@ -1,5 +1,6 @@
#include "StellarX.h" #include "StellarX.h"
#include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <memory> #include <memory>
@@ -88,6 +89,25 @@ int main(int argc, char** argv)
button->setTooltipText("stationary tip"); button->setTooltipText("stationary tip");
window.addControl(std::move(button)); window.addControl(std::move(button));
} }
else if (scenario == "input-probe")
{
// 该场景只提供一个可聚焦的 EasyX 顶层窗口,不让控件消费或改写键盘/IME 消息。
const std::array<std::string, 5> instructions{
"StellarX input probe - manual IME run",
"1. Click this window, then switch to the requested IME.",
"2. Type only the agreed non-sensitive test strings.",
"3. Cover commit, candidate select, cancel and language switch.",
"4. Close the window after the checklist is complete."
};
int y = 30;
for (const std::string& line : instructions)
{
auto label = std::make_unique<Label>(20, y, line, WHITE, Background);
label->setTextdisap(true);
window.addControl(std::move(label));
y += 42;
}
}
else if (scenario == "native-close") else if (scenario == "native-close")
{ {
// 只创建顶层窗口,由 UI 驱动发送原生关闭命令并验证宿主循环自行退出。 // 只创建顶层窗口,由 UI 驱动发送原生关闭命令并验证宿主循环自行退出。