mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-13 05:10:39 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19222f9cc9 | ||
|
|
e6e68f8d92 | ||
|
|
4c56b05894 | ||
|
|
a107ad22f8 | ||
|
|
3e2e27b77e | ||
|
|
2babfa6ced | ||
|
|
e9c52aa8cb |
@@ -186,6 +186,16 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- name: Checkout Windows test harness tooling
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: .ci-tools
|
||||
sparse-checkout: scripts/ensure-test-common-controls.ps1
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
@@ -234,7 +244,20 @@ jobs:
|
||||
|
||||
- name: Test Windows filesystem compatibility
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: cargo test --release --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked -- --test-threads=1
|
||||
shell: pwsh
|
||||
run: |
|
||||
$artifacts = Join-Path $env:RUNNER_TEMP 'codexx-test-artifacts.json'
|
||||
cargo test --release --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked --no-run --message-format=json > $artifacts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$harness = ./.ci-tools/scripts/ensure-test-common-controls.ps1 -CargoJson $artifacts -OutputDirectory $env:RUNNER_TEMP | ConvertFrom-Json
|
||||
Write-Host "Running all tests in $($harness.testExecutable)"
|
||||
$env:PATH = "$(Split-Path -Parent $harness.testExecutable);$env:PATH"
|
||||
Push-Location apps/desktop/src-tauri
|
||||
try {
|
||||
& $harness.testExecutable --test-threads=1
|
||||
$testExitCode = $LASTEXITCODE
|
||||
} finally { Pop-Location }
|
||||
if ($testExitCode -ne 0) { exit $testExitCode }
|
||||
|
||||
- name: Import Apple signing certificate and notarization key
|
||||
if: matrix.macos
|
||||
|
||||
@@ -44,7 +44,31 @@ jobs:
|
||||
run: |
|
||||
$openssl = Get-Command openssl -ErrorAction Stop
|
||||
& $openssl.Source version
|
||||
- run: cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked -- --test-threads=1
|
||||
- name: Run Windows release tests
|
||||
run: |
|
||||
$artifacts = Join-Path $env:RUNNER_TEMP 'codexx-test-artifacts.json'
|
||||
cargo test --release --manifest-path apps/desktop/src-tauri/Cargo.toml --lib --locked --no-run --message-format=json > $artifacts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$harness = ./scripts/ensure-test-common-controls.ps1 -CargoJson $artifacts -OutputDirectory $env:RUNNER_TEMP | ConvertFrom-Json
|
||||
Write-Host "Running all tests in $($harness.testExecutable)"
|
||||
$env:PATH = "$(Split-Path -Parent $harness.testExecutable);$env:PATH"
|
||||
Push-Location apps/desktop/src-tauri
|
||||
try {
|
||||
& $harness.testExecutable --test-threads=1
|
||||
$testExitCode = $LASTEXITCODE
|
||||
} finally { Pop-Location }
|
||||
if ($testExitCode -ne 0) { exit $testExitCode }
|
||||
- name: Diagnose failed Windows test loader
|
||||
if: failure()
|
||||
run: ./scripts/windows-dll-diagnostics.ps1 -RunHarnessList
|
||||
- name: Retain Windows loader diagnostics
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: windows-loader-diagnostics
|
||||
path: ${{ runner.temp }}/codexx-dll-diagnostics
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
- name: Build test installer without publishing or release signing credentials
|
||||
run: |
|
||||
Set-Content -Path "$env:RUNNER_TEMP\installer-check.json" -Value '{"bundle":{"createUpdaterArtifacts":false}}' -Encoding utf8
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
记录 Codex-X 的新功能、体验改进和问题修复。
|
||||
|
||||
## [v0.3.25] - 2026-10-09
|
||||
## [v0.3.26] - 2026-10-09
|
||||
|
||||
- 安装版在线更新的主通道失败后,自动通过官方 GitHub API 获取更新清单和安装包,继续由原生更新器检查版本、平台和签名;不再因一次检查失败就静默改为仅手动下载。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.3.25",
|
||||
"version": "0.3.26",
|
||||
"private": true,
|
||||
"description": "Codex Switch & Instruct desktop manager",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -492,7 +492,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-x"
|
||||
version = "0.3.25"
|
||||
version = "0.3.26"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.3.25"
|
||||
version = "0.3.26"
|
||||
description = "Codex Switch & Instruct desktop manager"
|
||||
authors = ["yynxxxxx"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -2359,6 +2359,8 @@ mod tests {
|
||||
let result = request(&proxy, &stream_payload());
|
||||
release.send(()).unwrap();
|
||||
let result = result.into_string().unwrap();
|
||||
// The client can receive EOF before the worker publishes final statistics.
|
||||
eventually(|| proxy.snapshot().in_flight == 0);
|
||||
assert!(result.contains("response.failed"));
|
||||
assert!(!result.contains("response.completed"));
|
||||
assert!(untouched.observed().is_empty());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.3.25",
|
||||
"version": "0.3.26",
|
||||
"identifier": "com.yynxxxxx.codexx",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.3.25",
|
||||
"version": "0.3.26",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#requires -Version 7.0
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$CargoJson,
|
||||
[string]$OutputDirectory = [System.IO.Path]::GetTempPath()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
|
||||
throw 'This helper only runs on the Windows CI runner.'
|
||||
}
|
||||
if ($env:GITHUB_ACTIONS -ne 'true' -or -not $env:RUNNER_TEMP) {
|
||||
throw 'This helper is restricted to an ephemeral Windows Actions runner.'
|
||||
}
|
||||
|
||||
# Cargo, not a filesystem wildcard or a caller-supplied EXE, selects the target.
|
||||
$executables = @(Get-Content -LiteralPath $CargoJson | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_)) {
|
||||
$record = ConvertFrom-Json -InputObject $_ -AsHashtable
|
||||
if ($record['reason'] -eq 'compiler-artifact' -and
|
||||
$record['target']['name'] -eq 'codexx_lib' -and
|
||||
$record['profile']['test'] -eq $true -and
|
||||
$record['executable'] -is [string]) {
|
||||
[System.IO.Path]::GetFullPath($record['executable'])
|
||||
}
|
||||
}
|
||||
} | Sort-Object -Unique)
|
||||
if ($executables.Count -ne 1) {
|
||||
throw "Expected one codexx_lib test executable in Cargo JSON; found $($executables.Count)."
|
||||
}
|
||||
$testExecutable = (Resolve-Path -LiteralPath $executables[0]).Path
|
||||
$testFile = Get-Item -LiteralPath $testExecutable
|
||||
if ($testFile.Name -notmatch '^codexx_lib-[0-9a-f]{8,32}\.exe$' -or
|
||||
$testFile.Directory.Name -ne 'deps' -or
|
||||
$testFile.Directory.Parent.Name -notin @('release', 'debug')) {
|
||||
throw "Refusing to modify a production or non-harness executable: $testExecutable"
|
||||
}
|
||||
|
||||
$sdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||||
$manifestTool = @(Get-ChildItem -LiteralPath $sdkRoot -Directory |
|
||||
Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } |
|
||||
Sort-Object { [version]$_.Name } -Descending |
|
||||
ForEach-Object { Join-Path $_.FullName 'x64\mt.exe' } |
|
||||
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
|
||||
Select-Object -First 1)
|
||||
if ($manifestTool.Count -ne 1) { throw 'Windows SDK x64 mt.exe was not found.' }
|
||||
$manifestTool = $manifestTool[0]
|
||||
|
||||
function Invoke-ManifestTool([string[]]$ToolArguments) {
|
||||
$start = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$start.FileName = $manifestTool
|
||||
$start.UseShellExecute = $false
|
||||
$start.RedirectStandardOutput = $true
|
||||
$start.RedirectStandardError = $true
|
||||
foreach ($argument in $ToolArguments) { $start.ArgumentList.Add($argument) }
|
||||
$manifestProcess = [System.Diagnostics.Process]::new()
|
||||
$manifestProcess.StartInfo = $start
|
||||
try {
|
||||
if (-not $manifestProcess.Start()) { throw 'Unable to start SDK mt.exe.' }
|
||||
$stdout = $manifestProcess.StandardOutput.ReadToEndAsync()
|
||||
$stderr = $manifestProcess.StandardError.ReadToEndAsync()
|
||||
$manifestProcess.WaitForExit()
|
||||
return @{ Code = $manifestProcess.ExitCode; Text = $stdout.Result + $stderr.Result }
|
||||
} finally { $manifestProcess.Dispose() }
|
||||
}
|
||||
function Require-ManifestTool([string[]]$ToolArguments) {
|
||||
$result = Invoke-ManifestTool $ToolArguments
|
||||
if ($result.Code -ne 0) { throw "mt.exe failed ($($result.Code)): $($result.Text)" }
|
||||
}
|
||||
|
||||
$reviewDirectory = Join-Path $OutputDirectory ('codexx-test-manifest-' + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $reviewDirectory | Out-Null
|
||||
$beforeManifest = Join-Path $reviewDirectory 'before.manifest'
|
||||
$additionManifest = Join-Path $reviewDirectory 'tauri-common-controls.manifest'
|
||||
$mergedManifest = Join-Path $reviewDirectory 'merged.manifest'
|
||||
$extractedManifest = Join-Path $reviewDirectory 'embedded.manifest'
|
||||
$backupExecutable = Join-Path $reviewDirectory ($testFile.Name + '.before')
|
||||
Copy-Item -LiteralPath $testExecutable -Destination $backupExecutable
|
||||
|
||||
$probe = Invoke-ManifestTool @('-nologo', "-inputresource:$testExecutable;#1", "-out:$beforeManifest")
|
||||
if ($probe.Code -eq 0) {
|
||||
$inputs = @($beforeManifest, $additionManifest)
|
||||
} elseif ($probe.Text -match '(?i)(The specified resource (type|name|language).*cannot be found|The specified image file.*did not contain a resource section)') {
|
||||
$inputs = @($additionManifest)
|
||||
} else {
|
||||
throw "Cannot safely extract the existing harness manifest: $($probe.Text)"
|
||||
}
|
||||
[System.IO.File]::WriteAllText((Join-Path $reviewDirectory 'extract-before.log'), $probe.Text)
|
||||
|
||||
$hasDefinitionIdentity = $false
|
||||
if ($probe.Code -eq 0) {
|
||||
[xml]$before = Get-Content -LiteralPath $beforeManifest -Raw
|
||||
$hasDefinitionIdentity = $null -ne $before.SelectSingleNode("/*[local-name()='assembly']/*[local-name()='assemblyIdentity']")
|
||||
}
|
||||
|
||||
# Same dependency tuple as tauri-build's default windows-app-manifest.xml.
|
||||
$addition = @'
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($additionManifest, $addition, [System.Text.UTF8Encoding]::new($false))
|
||||
if (-not $hasDefinitionIdentity) {
|
||||
[xml]$definition = $addition
|
||||
$identity = $definition.CreateElement('assemblyIdentity', $definition.DocumentElement.NamespaceURI)
|
||||
$identity.SetAttribute('type', 'win32')
|
||||
$identity.SetAttribute('name', 'Codex-X.TestHarness')
|
||||
$identity.SetAttribute('version', '1.0.0.0')
|
||||
$identity.SetAttribute('processorArchitecture', 'amd64')
|
||||
[void]$definition.DocumentElement.PrependChild($identity)
|
||||
[System.IO.File]::WriteAllText($additionManifest, $definition.OuterXml, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
Require-ManifestTool (@('-nologo', '-manifest') + $inputs + @("-out:$mergedManifest"))
|
||||
Require-ManifestTool @('-nologo', '-manifest', $mergedManifest, '-validate_manifest')
|
||||
Require-ManifestTool @('-nologo', '-manifest', $mergedManifest, "-outputresource:$testExecutable;#1")
|
||||
Require-ManifestTool @('-nologo', "-inputresource:$testExecutable;#1", "-out:$extractedManifest")
|
||||
[xml]$embedded = Get-Content -LiteralPath $extractedManifest -Raw
|
||||
$identity = $embedded.SelectSingleNode("//*[local-name()='dependency']/*[local-name()='dependentAssembly']/*[local-name()='assemblyIdentity'][@name='Microsoft.Windows.Common-Controls' and @version='6.0.0.0' and @publicKeyToken='6595b64144ccf1df']")
|
||||
if ($null -eq $identity) { throw 'Extracted harness manifest lacks the Common Controls v6 dependency.' }
|
||||
if ($null -eq $embedded.SelectSingleNode("/*[local-name()='assembly']/*[local-name()='assemblyIdentity']")) {
|
||||
throw 'Extracted harness manifest lacks its definition identity.'
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
testExecutable = $testExecutable
|
||||
manifestTool = $manifestTool
|
||||
hadExistingManifest = ($probe.Code -eq 0)
|
||||
originalExecutableBackup = $backupExecutable
|
||||
extractedManifest = $extractedManifest
|
||||
commonControlsV6Verified = $true
|
||||
testsExecutedByHelper = $false
|
||||
} | ConvertTo-Json
|
||||
@@ -0,0 +1,289 @@
|
||||
# Read-only loader diagnostics for an ephemeral Windows Actions runner.
|
||||
# Reads the test PE import/export tables; never invokes imported functions or
|
||||
# DLL DllMain. Optional --list starts only the Rust test harness, no test bodies.
|
||||
param(
|
||||
[string]$TestExe,
|
||||
[string]$OutputDirectory,
|
||||
[switch]$RunHarnessList
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:GITHUB_ACTIONS -ne 'true' -or -not $env:RUNNER_TEMP) {
|
||||
throw 'This diagnostic is restricted to an ephemeral Windows Actions runner.'
|
||||
}
|
||||
if (-not $TestExe) {
|
||||
$candidate = Get-ChildItem 'apps/desktop/src-tauri/target/release/deps/codexx_lib-*.exe' |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $candidate) { throw 'Compile cargo test --release --lib --no-run first.' }
|
||||
$TestExe = $candidate.FullName
|
||||
}
|
||||
$TestExe = (Resolve-Path -LiteralPath $TestExe).Path
|
||||
if ((Split-Path -Leaf $TestExe) -notlike 'codexx_lib-*.exe') {
|
||||
throw 'Only the Codex-X Rust test harness may be inspected.'
|
||||
}
|
||||
if (-not $OutputDirectory) {
|
||||
$OutputDirectory = Join-Path $env:RUNNER_TEMP 'codexx-dll-diagnostics'
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
|
||||
$ExeDirectory = Split-Path -Parent $TestExe
|
||||
$dumpbinCommand = Get-Command dumpbin.exe -ErrorAction SilentlyContinue
|
||||
if ($dumpbinCommand) { $Dumpbin = $dumpbinCommand.Source }
|
||||
else {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
if (-not (Test-Path -LiteralPath $vswhere)) { throw 'Neither dumpbin nor vswhere was found.' }
|
||||
$vs = (& $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath | Select-Object -First 1)
|
||||
$Dumpbin = (Get-ChildItem "$vs/VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe" |
|
||||
Sort-Object FullName -Descending | Select-Object -First 1).FullName
|
||||
if (-not $Dumpbin) { throw 'MSVC dumpbin.exe was not found.' }
|
||||
}
|
||||
$report = [ordered]@{
|
||||
TestExe = $TestExe
|
||||
TestSha256 = (Get-FileHash -LiteralPath $TestExe -Algorithm SHA256).Hash
|
||||
ImageOS = $env:ImageOS
|
||||
ImageVersion = $env:ImageVersion
|
||||
OS = [Environment]::OSVersion.VersionString
|
||||
Dumpbin = $Dumpbin
|
||||
Rust = ((& rustc --version --verbose) -join "`n")
|
||||
Cargo = ((& cargo --version) -join "`n")
|
||||
WorkingDirectory = (Get-Location).Path
|
||||
Path = $env:PATH
|
||||
Modules = @()
|
||||
Findings = @()
|
||||
Notes = @('DONT_RESOLVE_DLL_REFERENCES prevents DllMain and dependent imports from executing.',
|
||||
'PowerShell API-set resolution is recorded with the real module path.',
|
||||
'Candidates include test executable directory, System32, Windows and PATH; Cargo OUT_DIRs are appended for diagnostics.',
|
||||
'Missing named exports are verified with PE export tables and GetProcAddress; forwarders are followed.',
|
||||
'A finding is evidence to compare with the actual test process DLL search/activation context, not a guessed root cause.')
|
||||
}
|
||||
& $Dumpbin /imports $TestExe 2>&1 | Out-File (Join-Path $OutputDirectory 'test-imports.txt') -Encoding utf8
|
||||
& $Dumpbin /dependents $TestExe 2>&1 | Out-File (Join-Path $OutputDirectory 'test-dependents.txt') -Encoding utf8
|
||||
& $Dumpbin /headers $TestExe 2>&1 | Out-File (Join-Path $OutputDirectory 'test-headers.txt') -Encoding utf8
|
||||
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
public sealed class CodexxPeImport {
|
||||
public string Dll; public string Name; public ushort Ordinal; public bool ByOrdinal;
|
||||
}
|
||||
public sealed class CodexxPeExport { public bool Exists; public string Forwarder; }
|
||||
public static class CodexxLoaderProbe {
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||
public static extern IntPtr LoadLibraryExW(string name, IntPtr file, uint flags);
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||
public static extern uint GetModuleFileNameW(IntPtr module, StringBuilder path, int size);
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
|
||||
public static extern IntPtr GetProcAddress(IntPtr module, string name);
|
||||
[DllImport("kernel32.dll", EntryPoint="GetProcAddress", ExactSpelling=true, SetLastError=true)]
|
||||
public static extern IntPtr GetProcAddressOrdinal(IntPtr module, IntPtr ordinal);
|
||||
[DllImport("kernel32.dll")] public static extern bool FreeLibrary(IntPtr module);
|
||||
[DllImport("kernel32.dll")] public static extern uint SetErrorMode(uint mode);
|
||||
static Dictionary<string,byte[]> Files = new Dictionary<string,byte[]>(StringComparer.OrdinalIgnoreCase);
|
||||
static Dictionary<string,Dictionary<string,uint>> ExportNames = new Dictionary<string,Dictionary<string,uint>>(StringComparer.OrdinalIgnoreCase);
|
||||
static byte[] Bytes(string path) {
|
||||
byte[] value; if(Files.TryGetValue(path,out value)) return value;
|
||||
value=File.ReadAllBytes(path); Files[path]=value; return value;
|
||||
}
|
||||
static ushort U16(byte[] b,int p) { return BitConverter.ToUInt16(b,p); }
|
||||
static uint U32(byte[] b,int p) { return BitConverter.ToUInt32(b,p); }
|
||||
static ulong U64(byte[] b,int p) { return BitConverter.ToUInt64(b,p); }
|
||||
static int Optional(byte[] b) {
|
||||
if(b.Length<64 || b[0]!=0x4d || b[1]!=0x5a) throw new InvalidDataException("Not PE");
|
||||
int pe=checked((int)U32(b,60));
|
||||
if(pe+24>b.Length || U32(b,pe)!=0x4550) throw new InvalidDataException("Bad PE header");
|
||||
return pe+24;
|
||||
}
|
||||
static int Directory(byte[] b,int index) {
|
||||
int opt=Optional(b); ushort magic=U16(b,opt);
|
||||
if(magic!=0x20b && magic!=0x10b) throw new InvalidDataException("Unsupported PE");
|
||||
return opt+(magic==0x20b?112:96)+index*8;
|
||||
}
|
||||
static int Offset(byte[] b,uint rva) {
|
||||
int opt=Optional(b); int pe=opt-24;
|
||||
uint headerSize=U32(b,opt+60);
|
||||
if(rva<headerSize && rva<b.Length) return (int)rva;
|
||||
int section=opt+U16(b,pe+20), count=U16(b,pe+6);
|
||||
for(int n=0;n<count;n++,section+=40) {
|
||||
uint size=Math.Max(U32(b,section+8),U32(b,section+16));
|
||||
uint va=U32(b,section+12), raw=U32(b,section+20);
|
||||
if(rva>=va && (ulong)rva<(ulong)va+size) {
|
||||
ulong p=(ulong)raw+rva-va;
|
||||
if(p>=(ulong)b.Length) throw new InvalidDataException("RVA outside file");
|
||||
return checked((int)p);
|
||||
}
|
||||
}
|
||||
throw new InvalidDataException("Unmapped RVA");
|
||||
}
|
||||
static string Z(byte[] b,int p) {
|
||||
int end=p; while(end<b.Length && b[end]!=0 && end-p<4096) end++;
|
||||
if(end==b.Length || end-p==4096) throw new InvalidDataException("Unterminated PE string");
|
||||
return Encoding.ASCII.GetString(b,p,end-p);
|
||||
}
|
||||
public static CodexxPeImport[] Imports(string path) {
|
||||
byte[] b=Bytes(path); int dir=Directory(b,1); uint rva=U32(b,dir);
|
||||
var result=new List<CodexxPeImport>(); if(rva==0) return result.ToArray();
|
||||
int desc=Offset(b,rva); bool x64=U16(b,Optional(b))==0x20b;
|
||||
for(int d=0;d<4096;d++,desc+=20) {
|
||||
uint nameRva=U32(b,desc+12), thunkRva=U32(b,desc);
|
||||
if(nameRva==0) break; if(thunkRva==0) thunkRva=U32(b,desc+16);
|
||||
string dll=Z(b,Offset(b,nameRva)); int thunk=Offset(b,thunkRva);
|
||||
for(int n=0;n<100000;n++,thunk+=(x64?8:4)) {
|
||||
ulong v=x64?U64(b,thunk):U32(b,thunk); if(v==0) break;
|
||||
bool ordinal=(v & (x64?0x8000000000000000UL:0x80000000UL))!=0;
|
||||
result.Add(new CodexxPeImport { Dll=dll, ByOrdinal=ordinal,
|
||||
Ordinal=(ushort)(v & 65535), Name=ordinal?null:Z(b,Offset(b,checked((uint)v))+2) });
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
public static CodexxPeExport Export(string path,string wanted,ushort ordinal,bool byOrdinal) {
|
||||
byte[] b=Bytes(path); int dir=Directory(b,0);
|
||||
uint rva=U32(b,dir), size=U32(b,dir+4); var result=new CodexxPeExport();
|
||||
if(rva==0) return result; int exp=Offset(b,rva);
|
||||
uint first=U32(b,exp+16), count=U32(b,exp+20), names=U32(b,exp+24);
|
||||
int funcs=Offset(b,U32(b,exp+28)); uint index=UInt32.MaxValue;
|
||||
if(byOrdinal) { if(ordinal<first || ordinal-first>=count) return result; index=ordinal-first; }
|
||||
else {
|
||||
Dictionary<string,uint> lookup;
|
||||
if(!ExportNames.TryGetValue(path,out lookup)) {
|
||||
lookup=new Dictionary<string,uint>(StringComparer.Ordinal);
|
||||
int nameTable=Offset(b,U32(b,exp+32)), ordTable=Offset(b,U32(b,exp+36));
|
||||
for(uint n=0;n<names;n++) lookup[Z(b,Offset(b,U32(b,nameTable+checked((int)n*4))))]=U16(b,ordTable+checked((int)n*2));
|
||||
ExportNames[path]=lookup;
|
||||
}
|
||||
if(!lookup.TryGetValue(wanted,out index)) return result;
|
||||
}
|
||||
if(index==UInt32.MaxValue || index>=count) return result;
|
||||
uint target=U32(b,funcs+checked((int)index*4)); if(target==0) return result;
|
||||
result.Exists=true;
|
||||
if(target>=rva && (ulong)target<(ulong)rva+size) result.Forwarder=Z(b,Offset(b,target));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
$searchFolders = [Collections.Generic.List[string]]::new()
|
||||
$searchFolders.Add($ExeDirectory)
|
||||
$searchFolders.Add((Join-Path $env:SystemRoot 'System32'))
|
||||
$searchFolders.Add($env:SystemRoot)
|
||||
$searchFolders.Add((Get-Location).Path)
|
||||
foreach ($part in ($env:PATH -split ';')) { if ($part) { $searchFolders.Add($part.Trim('"')) } }
|
||||
# Cargo adds native build output directories while running a harness. Record all
|
||||
# candidates, but do not claim this broad diagnostic order is Cargo's exact order.
|
||||
Get-ChildItem 'apps/desktop/src-tauri/target/release/build/*/out' -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { $searchFolders.Add($_.FullName) }
|
||||
$knownDlls = @{}
|
||||
$known = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs'
|
||||
foreach ($property in $known.PSObject.Properties) {
|
||||
if ($property.Value -is [string] -and $property.Value -like '*.dll') { $knownDlls[$property.Value.ToLowerInvariant()] = $true }
|
||||
}
|
||||
$modules = @{}
|
||||
$findings = [Collections.Generic.List[object]]::new()
|
||||
function Get-ProbeModule([string]$Dll) {
|
||||
$key = $Dll.ToLowerInvariant()
|
||||
if ($modules.ContainsKey($key)) { return $modules[$key] }
|
||||
$candidates = @()
|
||||
foreach ($directory in $searchFolders) {
|
||||
$file = Join-Path $directory $Dll
|
||||
if (Test-Path -LiteralPath $file -PathType Leaf) { $candidates += $file }
|
||||
}
|
||||
$requested = $Dll
|
||||
if (-not ($key.StartsWith('api-ms-') -or $key.StartsWith('ext-ms-') -or $knownDlls.ContainsKey($key))) {
|
||||
if ($candidates.Count) { $requested = $candidates[0] }
|
||||
}
|
||||
$handle = [CodexxLoaderProbe]::LoadLibraryExW($requested,[IntPtr]::Zero,1)
|
||||
$errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
$resolved = $null
|
||||
if ($handle -ne [IntPtr]::Zero) {
|
||||
$buffer = [Text.StringBuilder]::new(4096)
|
||||
if ([CodexxLoaderProbe]::GetModuleFileNameW($handle,$buffer,$buffer.Capacity)) { $resolved = $buffer.ToString() }
|
||||
}
|
||||
$record = [ordered]@{ Requested=$Dll; LoadArgument=$requested; Path=$resolved; Handle=$handle; LoadError=$errorCode; Candidates=$candidates }
|
||||
if ($resolved) {
|
||||
$record['Version'] = (Get-Item -LiteralPath $resolved).VersionInfo.FileVersion
|
||||
$record['Sha256'] = (Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash
|
||||
& $Dumpbin /exports $resolved 2>&1 | Out-File (Join-Path $OutputDirectory ('exports-' + $Dll + '.txt')) -Encoding utf8
|
||||
}
|
||||
$modules[$key] = $record
|
||||
return $record
|
||||
}
|
||||
function Test-ProbeExport([string]$Dll,[string]$Name,[ushort]$Ordinal,[bool]$ByOrdinal,[string[]]$Chain=@()) {
|
||||
$symbol = if ($ByOrdinal) { "#$Ordinal" } else { $Name }
|
||||
$node = "$Dll!$symbol"
|
||||
if ($Chain.Count -ge 20 -or $node -in $Chain) {
|
||||
$findings.Add([ordered]@{ Kind='ForwarderCycle'; Import=$node; Chain=$Chain }); return
|
||||
}
|
||||
$Chain += $node
|
||||
$module = Get-ProbeModule $Dll
|
||||
if (-not $module.Path) {
|
||||
$findings.Add([ordered]@{ Kind='ModuleLoadFailure'; Import=$node; Win32=$module.LoadError; LoadArgument=$module.LoadArgument; Chain=$Chain }); return
|
||||
}
|
||||
try { $export = [CodexxLoaderProbe]::Export($module.Path,$Name,$Ordinal,$ByOrdinal) }
|
||||
catch {
|
||||
$findings.Add([ordered]@{ Kind='ExportParseFailure'; Import=$node; Path=$module.Path; Detail=$_.Exception.GetType().FullName; Chain=$Chain }); return
|
||||
}
|
||||
if (-not $export.Exists) {
|
||||
$findings.Add([ordered]@{ Kind='MissingExport'; Import=$node; Path=$module.Path; Chain=$Chain }); return
|
||||
}
|
||||
if ($export.Forwarder) {
|
||||
$split = $export.Forwarder.LastIndexOf('.')
|
||||
if ($split -lt 1) { $findings.Add([ordered]@{ Kind='MalformedForwarder'; Import=$node; Path=$module.Path; Forwarder=$export.Forwarder }); return }
|
||||
$targetDll = $export.Forwarder.Substring(0,$split)
|
||||
if (-not $targetDll.EndsWith('.dll',[StringComparison]::OrdinalIgnoreCase)) { $targetDll += '.dll' }
|
||||
$target = $export.Forwarder.Substring($split+1)
|
||||
if ($target.StartsWith('#')) { Test-ProbeExport $targetDll $null ([ushort]$target.Substring(1)) $true $Chain }
|
||||
else { Test-ProbeExport $targetDll $target 0 $false $Chain }
|
||||
return
|
||||
}
|
||||
$address = if ($ByOrdinal) { [CodexxLoaderProbe]::GetProcAddressOrdinal($module.Handle,[IntPtr]::new($Ordinal)) }
|
||||
else { [CodexxLoaderProbe]::GetProcAddress($module.Handle,$Name) }
|
||||
if ($address -eq [IntPtr]::Zero) {
|
||||
$findings.Add([ordered]@{ Kind='GetProcAddressFailure'; Import=$node; Path=$module.Path; Win32=[Runtime.InteropServices.Marshal]::GetLastWin32Error(); Chain=$Chain })
|
||||
}
|
||||
}
|
||||
try {
|
||||
$imports = [CodexxLoaderProbe]::Imports($TestExe)
|
||||
foreach ($import in $imports) { Test-ProbeExport $import.Dll $import.Name $import.Ordinal $import.ByOrdinal }
|
||||
# A STATUS_ENTRYPOINT_NOT_FOUND can be in a dependency's imports even when
|
||||
# the executable's direct imports exist. Traverse the resolved DLL closure.
|
||||
$visited = @{}
|
||||
$nestedCount = 0
|
||||
do {
|
||||
$pending = @($modules.Values | Where-Object { $_.Path -and -not $visited.ContainsKey($_.Path.ToLowerInvariant()) })
|
||||
foreach ($module in $pending) {
|
||||
$visited[$module.Path.ToLowerInvariant()] = $true
|
||||
if ($visited.Count -gt 512) { throw 'Dependency traversal exceeded its diagnostic bound.' }
|
||||
try { $nested = [CodexxLoaderProbe]::Imports($module.Path) }
|
||||
catch { $findings.Add([ordered]@{ Kind='ImportParseFailure'; Path=$module.Path; Detail=$_.Exception.GetType().FullName }); continue }
|
||||
$nestedCount += $nested.Count
|
||||
foreach ($import in $nested) { Test-ProbeExport $import.Dll $import.Name $import.Ordinal $import.ByOrdinal }
|
||||
}
|
||||
} while ($pending.Count -gt 0)
|
||||
$report['ImportCount'] = $imports.Count
|
||||
$report['NestedImportCount'] = $nestedCount
|
||||
$report['Findings'] = @($findings.ToArray())
|
||||
$report['Modules'] = @($modules.Values | ForEach-Object {
|
||||
[ordered]@{ Requested=$_.Requested; LoadArgument=$_.LoadArgument; Path=$_.Path; Version=$_.Version; Sha256=$_.Sha256; LoadError=$_.LoadError; Candidates=$_.Candidates }
|
||||
})
|
||||
if ($RunHarnessList) {
|
||||
$previousMode = [CodexxLoaderProbe]::SetErrorMode(0x8003)
|
||||
try {
|
||||
$start = @{
|
||||
FilePath=$TestExe; ArgumentList='--list'; PassThru=$true; NoNewWindow=$true; Wait=$true
|
||||
RedirectStandardOutput=(Join-Path $OutputDirectory 'harness-list.stdout.txt')
|
||||
RedirectStandardError=(Join-Path $OutputDirectory 'harness-list.stderr.txt')
|
||||
}
|
||||
$process = Start-Process @start
|
||||
$bits = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$process.ExitCode),0)
|
||||
$report['HarnessListExit'] = ('0x{0:X8}' -f $bits)
|
||||
} finally { [void][CodexxLoaderProbe]::SetErrorMode($previousMode) }
|
||||
}
|
||||
Copy-Item -LiteralPath $TestExe -Destination (Join-Path $OutputDirectory (Split-Path -Leaf $TestExe))
|
||||
$report | ConvertTo-Json -Depth 12 | Out-File (Join-Path $OutputDirectory 'diagnostics.json') -Encoding utf8
|
||||
Write-Host "DLL diagnostics: $OutputDirectory"
|
||||
Write-Host "Inspected $($imports.Count) imports; findings: $($findings.Count)"
|
||||
foreach ($finding in $findings) { Write-Host ($finding | ConvertTo-Json -Compress -Depth 8) }
|
||||
} finally {
|
||||
foreach ($module in $modules.Values) { if ($module.Handle -ne [IntPtr]::Zero) { [void][CodexxLoaderProbe]::FreeLibrary($module.Handle) } }
|
||||
}
|
||||
Reference in New Issue
Block a user