Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions .github/workflows/store-validation-trial.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
name: Store Validation Trial

on:
push:
branches:
- codex/store-validation
workflow_dispatch:

permissions:
contents: read

env:
BUILD_CONFIGURATION: Release
BUILD_PLATFORM: x64
DIST_DIR: dist/Q1View-windows-x64
STORE_VERSION: 1.0.16.0
STORE_PUBLISHER: CN=A89D24B3-A271-4AE1-9B9E-BFAE414EB0C6

jobs:
validate:
name: Install, WACK, and capture on Windows
runs-on: windows-2022
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v3

- name: Build applications
shell: pwsh
run: |
msbuild Viewer\Viewer.sln /m /restore /p:Configuration=$env:BUILD_CONFIGURATION /p:Platform=$env:BUILD_PLATFORM /p:PlatformToolset=v143
msbuild Comparer\Comparer.sln /m /restore /p:Configuration=$env:BUILD_CONFIGURATION /p:Platform=$env:BUILD_PLATFORM /p:PlatformToolset=v143
.\build\Package-Q1View.ps1 `
-Configuration $env:BUILD_CONFIGURATION `
-Platform $env:BUILD_PLATFORM `
-OutputDir $env:DIST_DIR

- name: Build Store submission MSIX
shell: pwsh
run: |
New-Item -ItemType Directory -Force "artifacts\store-submission" | Out-Null
.\build\Package-Q1ViewMsix.ps1 `
-SourceDir $env:DIST_DIR `
-OutputDir "dist" `
-AppVersion $env:STORE_VERSION `
-Publisher $env:STORE_PUBLISHER `
-SkipSigning
Copy-Item "dist\Q1View-windows-x64.msix" "artifacts\store-submission\Q1View-windows-x64.msix"

- name: Create certificate for sideload validation
id: dev_cert
shell: pwsh
run: |
New-Item -ItemType Directory -Force "artifacts\validation" | Out-Null
$cert = New-SelfSignedCertificate `
-Type Custom `
-Subject "CN=Q1ViewValidation" `
-KeyUsage DigitalSignature `
-FriendlyName "Q1View Store validation" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3")
$cerPath = Join-Path $PWD "artifacts\validation\Q1ViewValidation.cer"
Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null
Import-Certificate -FilePath $cerPath -CertStoreLocation "Cert:\LocalMachine\TrustedPeople" | Out-Null
"thumbprint=$($cert.Thumbprint)" | Out-File $env:GITHUB_OUTPUT -Encoding utf8 -Append

- name: Build signed validation MSIX
shell: pwsh
run: |
.\build\Package-Q1ViewMsix.ps1 `
-SourceDir $env:DIST_DIR `
-OutputDir "dist" `
-AppVersion $env:STORE_VERSION `
-Publisher "CN=Q1ViewValidation" `
-CertificateThumbprint "${{ steps.dev_cert.outputs.thumbprint }}"
Copy-Item "dist\Q1View-windows-x64.msix" "artifacts\validation\Q1View-validation.msix"
Get-AuthenticodeSignature "artifacts\validation\Q1View-validation.msix" |
Format-List * | Out-File "artifacts\validation\signature.txt"

- name: Install package and record identity
shell: powershell
run: |
Add-AppxPackage -Path "artifacts\validation\Q1View-validation.msix"
$pkg = Get-AppxPackage -Name "KyuwonKim.Q1View"
if (-not $pkg) {
throw "The installed Q1View validation package was not found."
}
$pkg | Format-List Name, PackageFullName, PackageFamilyName, InstallLocation, Status |
Out-File "artifacts\validation\installed-package.txt"

- name: Prepare screenshot input images
shell: pwsh
run: |
choco install imagemagick.app --no-progress --yes
New-Item -ItemType Directory -Force "artifacts\screenshots\input" | Out-Null
$magick = (Get-Command magick.exe -ErrorAction Stop).Source
& $magick "docs\images\viewer-video.webp" -crop "960x540+100+108" +repage "artifacts\screenshots\input\reference.png"
& $magick "artifacts\screenshots\input\reference.png" -quality 72 "artifacts\screenshots\input\encoded.jpg"
if ($LASTEXITCODE -ne 0) {
throw "Failed to make screenshot input images."
}

- name: Capture packaged-build application windows
shell: powershell
run: |
New-Item -ItemType Directory -Force "artifacts\screenshots" | Out-Null
Import-Module ServerCore -ErrorAction SilentlyContinue
if (Get-Command Set-DisplayResolution -ErrorAction SilentlyContinue) {
Set-DisplayResolution -Width 1920 -Height 1080 -Force
}

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class Q1WindowTools {
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
}
"@

$reference = (Resolve-Path "artifacts\screenshots\input\reference.png").Path
$encoded = (Resolve-Path "artifacts\screenshots\input\encoded.jpg").Path

function Capture-AppWindow([string]$appId, [string]$arguments, [string]$destination) {
$exePath = Join-Path $env:DIST_DIR "$appId.exe"
$process = Start-Process -FilePath $exePath -ArgumentList $arguments -PassThru
try {
foreach ($attempt in 1..20) {
Start-Sleep -Milliseconds 500
$process.Refresh()
if ($process.HasExited) {
"$appId exit code before capture: $($process.ExitCode)" |
Out-File "artifacts\screenshots\capture-result.txt" -Append
throw "$appId exited before its window could be captured (exit code $($process.ExitCode))."
}
if ($process.MainWindowHandle -ne 0) {
break
}
}
if ($process.MainWindowHandle -eq 0) {
throw "No visible window was created for $appId."
}
[Q1WindowTools]::ShowWindowAsync($process.MainWindowHandle, 3) | Out-Null
Start-Sleep -Seconds 3
$process.Refresh()
if ($process.HasExited) {
"$appId exit code after open: $($process.ExitCode)" |
Out-File "artifacts\screenshots\capture-result.txt" -Append
throw "$appId exited after opening its command-line source input (exit code $($process.ExitCode))."
}

$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
"$appId primary screen: $($bounds.Width)x$($bounds.Height)" |
Out-File "artifacts\screenshots\display.txt" -Append
if ($bounds.Width -lt 1366 -or $bounds.Height -lt 768) {
throw "Screen resolution $($bounds.Width)x$($bounds.Height) is below Store screenshot minimum."
}
$bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bitmap.Save($destination, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$graphics.Dispose()
$bitmap.Dispose()
}
"$appId remained running through screenshot capture." |
Out-File "artifacts\screenshots\capture-result.txt" -Append
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force
}
}
}

Capture-AppWindow "Viewer" "`"$reference`"" "artifacts\screenshots\viewer.png"
Capture-AppWindow "Comparer" "`"$reference`" `"$encoded`"" "artifacts\screenshots\comparer.png"

$viewerPath = (Resolve-Path (Join-Path $env:DIST_DIR "Viewer.exe")).Path
$viewerProcess = Start-Process -FilePath $viewerPath `
-WorkingDirectory $env:RUNNER_TEMP -ArgumentList "`"$reference`"" -PassThru
$comparerProcess = $null
try {
foreach ($attempt in 1..20) {
Start-Sleep -Milliseconds 500
$viewerProcess.Refresh()
if ($viewerProcess.HasExited) {
throw "Viewer exited before its Compare action could be tested."
}
if ($viewerProcess.MainWindowHandle -ne 0) {
break
}
}
if ($viewerProcess.MainWindowHandle -eq 0) {
throw "Viewer did not create a window for its Compare action test."
}
$compareCommand = [UIntPtr]::new([uint32]32771)
[Q1WindowTools]::SendMessage($viewerProcess.MainWindowHandle, 0x0111, $compareCommand, [IntPtr]::Zero) | Out-Null
foreach ($attempt in 1..20) {
Start-Sleep -Milliseconds 500
$comparerProcess = Get-Process -Name "Comparer" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($comparerProcess -and $comparerProcess.MainWindowHandle -ne 0) {
break
}
}
if (-not $comparerProcess -or $comparerProcess.MainWindowHandle -eq 0) {
throw "Viewer's Compare action did not open the packaged Comparer executable."
}
"Viewer Compare action opened the packaged Comparer executable from a different working directory." |
Out-File "artifacts\screenshots\capture-result.txt" -Append
}
finally {
if ($comparerProcess -and -not $comparerProcess.HasExited) {
Stop-Process -Id $comparerProcess.Id -Force
}
if (-not $viewerProcess.HasExited) {
Stop-Process -Id $viewerProcess.Id -Force
}
}

- name: Uninstall smoke-tested package
shell: powershell
run: |
$pkg = Get-AppxPackage -Name "KyuwonKim.Q1View"
if (-not $pkg) {
throw "Q1View package disappeared before uninstall testing."
}
Remove-AppxPackage -Package $pkg.PackageFullName
if (Get-AppxPackage -Name "KyuwonKim.Q1View") {
throw "Q1View package was still installed after Remove-AppxPackage."
}
"Package installed and uninstalled cleanly." |
Out-File "artifacts\validation\install-uninstall-result.txt"

- name: Run Windows App Certification Kit
timeout-minutes: 30
shell: pwsh
run: |
$appCert = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit" `
-Filter appcert.exe -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $appCert) {
throw "appcert.exe was not found on the Windows runner."
}
"Using WACK: $($appCert.FullName)" | Out-File "artifacts\validation\wack-command.txt"
& $appCert.FullName reset
& $appCert.FullName test `
-appxpackagepath (Resolve-Path "artifacts\validation\Q1View-validation.msix").Path `
-reportoutputpath (Join-Path $PWD "artifacts\validation\WACKReport.xml")
if ($LASTEXITCODE -ne 0) {
throw "WACK failed with exit code $LASTEXITCODE. See WACKReport.xml."
}
[xml]$report = Get-Content "artifacts\validation\WACKReport.xml"
$failedTests = @($report.REPORT.REQUIREMENTS.REQUIREMENT.TEST |
Where-Object { $_.RESULT.InnerText -eq "FAIL" })
"WACK overall result: $($report.REPORT.OVERALL_RESULT)" |
Out-File "artifacts\validation\wack-summary.txt"
foreach ($test in $failedTests) {
$failure = "WACK failed test: $($test.NAME) (optional=$($test.OPTIONAL))"
Write-Warning $failure
$failure | Out-File "artifacts\validation\wack-summary.txt" -Append
}
$blockingFailures = @($failedTests | Where-Object {
$_.OPTIONAL -ne "TRUE" -or $_.NAME -eq "Application count"
})
if ($blockingFailures.Count -gt 0) {
throw "WACK reported a Store-blocking failure. See WACKReport.xml."
}

- name: Upload validation output
if: always()
uses: actions/upload-artifact@v6
with:
name: Q1View-store-validation-trial
path: |
artifacts/store-submission/
artifacts/screenshots/
artifacts/validation/WACKReport.xml
artifacts/validation/signature.txt
artifacts/validation/installed-package.txt
artifacts/validation/install-uninstall-result.txt
artifacts/validation/wack-command.txt
artifacts/validation/wack-summary.txt
38 changes: 37 additions & 1 deletion Comparer/Comparer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ CComparerApp::CComparerApp()

CComparerApp theApp;

class CComparerCommandLineInfo : public CCommandLineInfo
{
public:
CString m_strFilesToOpen;

virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast)
{
if (!bFlag && (m_nShellCommand == FileNew || !m_strFilesToOpen.IsEmpty())) {
if (!m_strFilesToOpen.IsEmpty())
m_strFilesToOpen += CSV_SEPARATOR;
m_strFilesToOpen += pszParam;

if (bLast) {
// Let MFC create the SDI frame without treating a multi-file
// list as one shell/MRU path. The app opens these files below.
m_nShellCommand = FileNew;
m_bShowSplash = FALSE;
}
return;
}

CCommandLineInfo::ParseParam(pszParam, bFlag, bLast);
}
};


// CComparerApp initialization

Expand All @@ -55,7 +80,7 @@ BOOL CComparerApp::InitInstance()


// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
CComparerCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);


Expand All @@ -67,6 +92,17 @@ BOOL CComparerApp::InitInstance()
// The one and only window has been initialized, so show and update it
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();

if (!cmdInfo.m_strFilesToOpen.IsEmpty()) {
CMainFrame *pMainFrm = static_cast<CMainFrame *>(m_pMainWnd);
CComparerDoc *pDoc = static_cast<CComparerDoc *>(pMainFrm->GetActiveDocument());
if (pDoc == NULL)
return FALSE;

pDoc->mPendingFile = cmdInfo.m_strFilesToOpen;
pMainFrm->PostMessage(WM_OPEN_PENDING_FILE);
}

// call DragAcceptFiles only if there's a suffix
// In an SDI app, this should occur after ProcessShellCommand
return TRUE;
Expand Down
13 changes: 9 additions & 4 deletions Comparer/ComparerDoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ CComparerDoc::CComparerDoc()
, mD(0.f)
, mN(ZOOM_RATIO(mD))
, mFrmState("")
, mPosInfoView(NULL)
, mFrmsInfoView(NULL)
, mMaxFrames(0)
, mMinFrames(0)
, mFrmCmpStrategy(NULL)
Expand Down Expand Up @@ -519,11 +521,14 @@ BOOL CComparerDoc::OnOpenDocument(LPCTSTR lpszPathName)
CMainFrame *pMainFrm = static_cast<CMainFrame *>(AfxGetMainWnd());
CString fileList = lpszPathName == NULL ? _T("") : lpszPathName;

if (pMainFrm == NULL) {
// Command-line open can reach here before the SDI frame exists.
// Defer the real open until CMainFrame::ActivateFrame().
if (pMainFrm == NULL || !pMainFrm->IsWindowVisible()) {
// A startup shell-open can run before the frame views have received
// their initial layout. Delay loading until there is a drawable pane.
mPendingFile = fileList;
::CoInitialize(NULL);
if (pMainFrm == NULL)
::CoInitialize(NULL);
else
pMainFrm->PostMessage(WM_OPEN_PENDING_FILE);
return TRUE;
}

Expand Down
8 changes: 7 additions & 1 deletion Comparer/ComparerView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
// CComparerView

CComparerView::CComparerView()
: mIsClicked(false)
: mXDst(0)
, mYDst(0)
, mWClient(0)
, mHClient(0)
, mWCanvas(0)
, mHCanvas(0)
, mIsClicked(false)
, mProcessing(false)
, mRgbBufSize(0)
, mRgbBuf(NULL)
Expand Down
Loading