feat(diagnostic): 阶段 0A 输入链路被动探测与线程归属复现
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
<!-- Normal and sanitizer outputs use separate directories so ASan builds stay isolated. -->
|
||||
<OutDir>$(SolutionDir)build\$(Platform)\$(Configuration)\</OutDir>
|
||||
<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 Condition="'$(EnableAsan)'=='true' And '$(Platform)'=='x64'">
|
||||
<EnableASAN>true</EnableASAN>
|
||||
@@ -45,18 +47,23 @@
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)include\StellarX;$(ProjectDir)src\StellarX\internal;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</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. -->
|
||||
<ItemGroup>
|
||||
<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\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" />
|
||||
</ItemGroup>
|
||||
<!-- Framework source files compiled into StellarX.Core.lib. -->
|
||||
<ItemGroup>
|
||||
<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\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" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
@@ -11,6 +11,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.Tests", "tests\Ste
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StellarX.UiFixture", "tests\StellarX.UiFixture.vcxproj", "{D0AF4B0B-7379-4A20-BF10-68CC8DA4D905}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EasyX.ThreadProbe", "tests\EasyX.ThreadProbe.vcxproj", "{6D338118-A536-4E4F-A4F5-3C73C5CA107A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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).'
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Dialog.h"
|
||||
#include "SxLog.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
#include "SxInputProbeInternal.h"
|
||||
|
||||
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)
|
||||
@@ -187,6 +188,7 @@ void Dialog::Show()
|
||||
{
|
||||
show = false;
|
||||
hWnd.dialogOpen = false;
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
|
||||
return;
|
||||
}
|
||||
int lastW = rc0.right - rc0.left;
|
||||
@@ -209,6 +211,7 @@ void Dialog::Show()
|
||||
{
|
||||
show = false;
|
||||
hWnd.dialogOpen = false;
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
|
||||
break;
|
||||
}
|
||||
const int cw = rc.right - rc.left;
|
||||
@@ -232,17 +235,22 @@ void Dialog::Show()
|
||||
|
||||
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY)
|
||||
ExMessage msg;
|
||||
auto probeBatch = StellarX::Internal::inputProbeHostBatchBegin(hWnd.getHwnd(), true);
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(msg,
|
||||
[](ExMessage& next) { return peekmessage(&next, EX_MOUSE | EX_KEY); },
|
||||
[&](const ExMessage& next) {
|
||||
StellarX::Internal::inputProbeObserveEasyXMessage(hWnd.getHwnd(), probeBatch, next);
|
||||
handleEvent(next);
|
||||
return show;
|
||||
});
|
||||
processedMessages = static_cast<int>(batch.processed);
|
||||
StellarX::Internal::inputProbeHostBatchEnd(
|
||||
hWnd.getHwnd(), probeBatch, batch.processed, batch.stopped);
|
||||
if (!hWnd.hasLiveNativeWindow())
|
||||
{
|
||||
show = false;
|
||||
hWnd.dialogOpen = false;
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -254,6 +262,8 @@ void Dialog::Show()
|
||||
EndBatchDraw();
|
||||
dirty = false;
|
||||
}
|
||||
if (!hWnd.isMutationTransactionActive())
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd.getHwnd());
|
||||
|
||||
if (batch.shouldSleep())
|
||||
Sleep(1);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+32
-9
@@ -2,6 +2,7 @@
|
||||
#include "Dialog.h"
|
||||
#include"SxLog.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
#include "SxInputProbeInternal.h"
|
||||
#include <easyx.h>
|
||||
#include <algorithm>
|
||||
// 可能频繁出现且对调试信息干扰较大的消息(例如鼠标移动),
|
||||
@@ -679,6 +680,7 @@ Window::~Window()
|
||||
dialogs.clear();
|
||||
controls.clear();
|
||||
background.reset();
|
||||
StellarX::Internal::inputProbeDetach(hWnd);
|
||||
if (hWnd && procHooked && oldWndProc && hasLiveNativeWindow())
|
||||
{
|
||||
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);
|
||||
}
|
||||
const auto probeCall = StellarX::Internal::inputProbeWndProcEnter(h, m, w, l);
|
||||
|
||||
// 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁
|
||||
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");
|
||||
self->isSizing = true;
|
||||
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;
|
||||
if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000)
|
||||
{
|
||||
return TRUE;
|
||||
return StellarX::Internal::inputProbeWndProcLeave(probeCall, TRUE, false);
|
||||
}
|
||||
|
||||
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("之后=(","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")";
|
||||
}
|
||||
return TRUE;
|
||||
return StellarX::Internal::inputProbeWndProcLeave(probeCall, TRUE, false);
|
||||
}
|
||||
|
||||
// 关键点④:拉伸结束 → 解冻重绘 + 统一收口(记录最终尺寸 -> 标记 needResizeDirty)
|
||||
@@ -768,7 +771,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
||||
// 立即解冻重绘标志,同时标记区域为有效,避免触发额外 WM_PAINT。
|
||||
SendMessage(h, WM_SETREDRAW, TRUE, 0);
|
||||
ValidateRect(h, nullptr);
|
||||
return 0;
|
||||
return StellarX::Internal::inputProbeWndProcLeave(probeCall, 0, false);
|
||||
}
|
||||
|
||||
// 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合)
|
||||
@@ -782,20 +785,23 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
||||
AdjustWindowRectEx(&rc, style, FALSE, ex);
|
||||
mmi->ptMinTrackSize.x = rc.right - rc.left;
|
||||
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)
|
||||
: DefWindowProc(h, m, w, l);
|
||||
const LRESULT observedResult = StellarX::Internal::inputProbeWndProcLeave(
|
||||
probeCall, result, forwardedToOldProc);
|
||||
if (m == WM_NCDESTROY)
|
||||
{
|
||||
// 必须放在旧窗口过程返回之后,并作为本函数最后一次 self 访问。
|
||||
// 宿主线程看到 false 后即可安全离开 runEventLoop 并析构 Window。
|
||||
self->nativeWindowAlive.store(false, std::memory_order_release);
|
||||
}
|
||||
return result;
|
||||
return observedResult;
|
||||
}
|
||||
|
||||
// ---------------- 绘制 ----------------
|
||||
@@ -823,8 +829,11 @@ void Window::draw()
|
||||
if (!procHooked)
|
||||
{
|
||||
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
|
||||
StellarX::Internal::inputProbeAttach(hWnd);
|
||||
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
|
||||
procHooked = (oldWndProc != nullptr);
|
||||
if (!procHooked)
|
||||
StellarX::Internal::inputProbeDetach(hWnd);
|
||||
}
|
||||
if (!headline.empty())
|
||||
{
|
||||
@@ -860,8 +869,11 @@ void Window::draw(std::string imagePath)
|
||||
if (!procHooked)
|
||||
{
|
||||
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
|
||||
StellarX::Internal::inputProbeAttach(hWnd);
|
||||
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
|
||||
procHooked = (oldWndProc != nullptr);
|
||||
if (!procHooked)
|
||||
StellarX::Internal::inputProbeDetach(hWnd);
|
||||
}
|
||||
|
||||
bkImageFile = std::move(imagePath);
|
||||
@@ -909,12 +921,17 @@ int Window::runEventLoop()
|
||||
while (true)
|
||||
{
|
||||
if (!hasLiveNativeWindow())
|
||||
{
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// drainMessageBatch 将一轮消息限制在固定上限;批次结束后统一处理 resize、tick 和重绘。
|
||||
auto probeBatch = StellarX::Internal::inputProbeHostBatchBegin(hWnd, false);
|
||||
const auto batch = StellarX::Internal::drainMessageBatch(msg,
|
||||
[](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; // 每条消息独立判断,不能把上一条消息的消费状态带入下一条。
|
||||
if (msg.message == WM_CLOSE)
|
||||
return false;
|
||||
@@ -1027,8 +1044,12 @@ int Window::runEventLoop()
|
||||
flushPendingMutationsInTree();
|
||||
return true;
|
||||
});
|
||||
StellarX::Internal::inputProbeHostBatchEnd(hWnd, probeBatch, batch.processed, batch.stopped);
|
||||
if (batch.stopped || !hasLiveNativeWindow())
|
||||
{
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ULONGLONG nowMs = GetTickCount64();
|
||||
// tick 独立于鼠标消息推进 Tooltip 等延时状态,仍放在事务内避免回调立即改 vector。
|
||||
@@ -1188,6 +1209,8 @@ int Window::runEventLoop()
|
||||
}
|
||||
|
||||
sweepClosedDialogs();
|
||||
if (!isMutationTransactionActive())
|
||||
StellarX::Internal::inputProbeFlushAtSafePoint(hWnd);
|
||||
|
||||
// 只在真正空闲时短暂让出 CPU;有消息的批次不得人为限速。
|
||||
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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "StellarX.h"
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
@@ -88,6 +89,25 @@ int main(int argc, char** argv)
|
||||
button->setTooltipText("stationary tip");
|
||||
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")
|
||||
{
|
||||
// 只创建顶层窗口,由 UI 驱动发送原生关闭命令并验证宿主循环自行退出。
|
||||
|
||||
Reference in New Issue
Block a user