feat(diagnostic): 阶段 0A 输入链路被动探测与线程归属复现
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 "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
@@ -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
|
||||||
|
}
|
||||||
@@ -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 "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 驱动发送原生关闭命令并验证宿主循环自行退出。
|
||||||
|
|||||||
Reference in New Issue
Block a user