forked from Manuel/meeting-assistant
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
147c544496 |
No files matched your search
@@ -3,11 +3,6 @@ name: PR and Push Build/Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
# This temporary branch changes only the native prerequisite diagnostic.
|
||||
# Its manual workflow provides that evidence; the PR branch still runs all jobs.
|
||||
branches-ignore:
|
||||
- codex/macos-ci-kvm-compatibility
|
||||
- codex/macos-kvm-noavx-recovery
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -117,6 +112,15 @@ jobs:
|
||||
--nologo
|
||||
test -s MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll
|
||||
|
||||
- name: Measure transcript reader sharing on the Windows Wine host
|
||||
run: |
|
||||
mkdir -p artifacts/tests
|
||||
transcript_probe_exit=0
|
||||
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" run \
|
||||
--file tools/ci/TranscriptFileShareProbe.cs > artifacts/tests/transcript-file-sharing.json || transcript_probe_exit=$?
|
||||
cat artifacts/tests/transcript-file-sharing.json
|
||||
exit "${transcript_probe_exit}"
|
||||
|
||||
- name: Run tests via Wine (Windows dotnet host)
|
||||
run: |
|
||||
rm -f artifacts/tests/wine.trx
|
||||
|
||||
@@ -191,18 +191,21 @@ public sealed class RecordingCoordinatorTests
|
||||
NullLogger<MarkdownMeetingNoteStore>.Instance);
|
||||
var artifactStore = new MarkdownMeetingArtifactStore(
|
||||
NullLogger<MarkdownMeetingArtifactStore>.Instance);
|
||||
var writeProbe = new TranscriptWriteDiagnostic();
|
||||
var transcriptStore = new DiagnosticTranscriptStore(
|
||||
new VaultTranscriptStore(Options.Create(options), NullLogger<VaultTranscriptStore>.Instance),
|
||||
writeProbe);
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(
|
||||
new DiagnosticTranscriptionProvider(
|
||||
new FixedSegmentStreamingTranscriptionProvider(
|
||||
new TranscriptionSegment(
|
||||
TimeSpan.FromSeconds(4),
|
||||
TimeSpan.FromSeconds(5),
|
||||
"Guest-1",
|
||||
"Azure returned ***** here."))),
|
||||
new VaultTranscriptStore(
|
||||
Options.Create(options),
|
||||
NullLogger<VaultTranscriptStore>.Instance),
|
||||
"Azure returned ***** here.")), writeProbe)),
|
||||
transcriptStore,
|
||||
noteStore,
|
||||
new CapturingMeetingNoteOpener(),
|
||||
artifactStore,
|
||||
@@ -218,9 +221,45 @@ public sealed class RecordingCoordinatorTests
|
||||
NullLogger<MeetingWorkflowEngine>.Instance));
|
||||
|
||||
var started = await coordinator.StartAsync(CancellationToken.None);
|
||||
Exception? waitFailure = null;
|
||||
Exception? stopFailure = null;
|
||||
try
|
||||
{
|
||||
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
|
||||
writeProbe.Record("test-audio-enqueued");
|
||||
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "Azure returned"));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
waitFailure = exception;
|
||||
writeProbe.Record("test-wait-failed", exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
writeProbe.Record("test-stop-completed");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
stopFailure = exception;
|
||||
writeProbe.Record("test-stop-failed", exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (waitFailure is TimeoutException)
|
||||
{
|
||||
throw new TimeoutException($"{waitFailure.Message} {writeProbe.Describe()}", waitFailure);
|
||||
}
|
||||
if (waitFailure is not null)
|
||||
{
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(waitFailure).Throw();
|
||||
}
|
||||
if (stopFailure is not null)
|
||||
{
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(stopFailure).Throw();
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(started.TranscriptPath!);
|
||||
Assert.Contains("[00:00:04] Guest-1: Azure returned [redacted] here.", content);
|
||||
@@ -5212,6 +5251,94 @@ public sealed class RecordingCoordinatorTests
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary diagnosis of Run4174. Observes public provider/store boundaries only.
|
||||
private sealed class TranscriptWriteDiagnostic
|
||||
{
|
||||
private readonly System.Diagnostics.Stopwatch elapsed = System.Diagnostics.Stopwatch.StartNew();
|
||||
private readonly ConcurrentQueue<string> events = new();
|
||||
|
||||
public void Record(string name, Exception? error = null)
|
||||
{
|
||||
events.Enqueue($"{elapsed.ElapsedMilliseconds}ms:{name}" + (error is null ? "" :
|
||||
$":{error.GetType().FullName}:HResult=0x{error.HResult:X8}:{error.Message}"));
|
||||
}
|
||||
|
||||
public string Describe() => "[DEBUG-transcript-write-4174] " + string.Join(" | ", events);
|
||||
}
|
||||
|
||||
private sealed class DiagnosticTranscriptionProvider(
|
||||
IStreamingTranscriptionProvider inner,
|
||||
TranscriptWriteDiagnostic probe) : IStreamingTranscriptionProvider
|
||||
{
|
||||
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
|
||||
IAsyncEnumerable<AudioChunk> audio,
|
||||
SpeechRecognitionPipelineOptions options,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var segment in inner.TranscribeAsync(ObserveAudioAsync(audio, cancellationToken), options, cancellationToken))
|
||||
{
|
||||
probe.Record("fake-segment-yielded");
|
||||
yield return segment;
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<AudioChunk> ObserveAudioAsync(
|
||||
IAsyncEnumerable<AudioChunk> audio,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var chunk in audio.WithCancellation(cancellationToken))
|
||||
{
|
||||
probe.Record("fake-audio-consumed");
|
||||
yield return chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DiagnosticTranscriptStore(
|
||||
ITranscriptStore inner,
|
||||
TranscriptWriteDiagnostic probe) : ITranscriptStore
|
||||
{
|
||||
public Task<TranscriptSession> CreateSessionAsync(CancellationToken cancellationToken) =>
|
||||
inner.CreateSessionAsync(cancellationToken);
|
||||
public Task<TranscriptSession> CreateSessionAsync(MeetingAssistantOptions options, DateTimeOffset startedAt, CancellationToken cancellationToken) =>
|
||||
inner.CreateSessionAsync(options, startedAt, cancellationToken);
|
||||
public Task ReplaceLinesAsync(TranscriptSession session, IReadOnlyList<string> replacementLines, CancellationToken cancellationToken) =>
|
||||
inner.ReplaceLinesAsync(session, replacementLines, cancellationToken);
|
||||
public Task UpdateMetadataAsync(TranscriptSession session, MeetingSessionArtifacts artifacts, MeetingNote meetingNote, CancellationToken cancellationToken) =>
|
||||
inner.UpdateMetadataAsync(session, artifacts, meetingNote, cancellationToken);
|
||||
|
||||
public async Task<TranscriptLineReference> AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||
{
|
||||
probe.Record("append-entered");
|
||||
try
|
||||
{
|
||||
var reference = await inner.AppendLineAsync(session, line, cancellationToken);
|
||||
probe.Record("append-completed");
|
||||
return reference;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
probe.Record("append-failed", exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReplaceLineAsync(TranscriptSession session, TranscriptLineReference lineReference, string replacementLine, CancellationToken cancellationToken)
|
||||
{
|
||||
probe.Record("rewrite-entered");
|
||||
try
|
||||
{
|
||||
await inner.ReplaceLineAsync(session, lineReference, replacementLine, cancellationToken);
|
||||
probe.Record("rewrite-completed");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
probe.Record("rewrite-failed", exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSpeechRecognitionPipelineFactory : ISpeechRecognitionPipelineFactory
|
||||
{
|
||||
private readonly IStreamingTranscriptionProvider provider;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
# macOS 13 KVM/Cryptex/NoAVX compatibility diagnostic
|
||||
# macOS 13 KVM/Cryptex compatibility diagnostic
|
||||
|
||||
This separate manual candidate probes Recovery readiness on the existing Ubuntu Docker daemon with KVM, the real Intel host CPU and macOS 13. It does not install macOS, erase a disk, install .NET or Apple CLT, or run Meeting Assistant. Passing proves only a fresh macOS 13+ x86_64 Recovery guest with root identity, a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk.
|
||||
|
||||
Baseline: bootstrap commit `4606de069678e8f95dfe3c7dad1bf5ce5384d30c`; separate branch `codex/macos-ci-kvm-compatibility`. KVM, CPU passthrough, Recovery major version and guest Cryptex staging change together. This is a compatibility experiment, not a causal single-variable A/B test. The TCG/bootstrap experiment remains separate.
|
||||
|
||||
The NoAVX continuation compares against KVM Recovery commit `720a431`. Its only guest change is adding `NoAVXFSCompressionTypeZlib-AVXpel.kext` to the existing OpenCore overlay and `Kernel.Add`. Existing Lilu/CryptexFixup, CPU passthrough, macOS 13 Recovery, disk, probes and deadlines are preserved. The hypothesis is that an AVX-dependent filesystem decompression path blocks native file loading on the Celeron; this has not been established as the cause of the disk-readiness hang. Application source is unchanged, and this candidate has only offline validation evidence.
|
||||
|
||||
The host-memory admission check is copied unchanged from RAW candidate `ec5508e`: the unchanged 4-GiB guest plus 512 MiB QEMU overhead requires 4.5 GiB available. Offline validation accepts the captured run-4204 value of 5,138,696 KiB and rejects 4 GiB, missing and invalid values. Guest RAM and the 6-GiB container cap are unchanged. This admission budget reserves no host memory against other workloads. The `codex/macos-kvm-noavx-recovery` branch skips only the PR/Push workflow's push trigger; pull requests and manual workflows retain their existing triggers.
|
||||
|
||||
The existing one-minute heartbeat prints at most the last two captured `[proof-start]`, `[proof-done]`, `[proof-native-wait]`, `[proof-result]` or `[native-version]` lines, each capped at 256 characters. `[proof-native-wait]` is this candidate's existing command-completion marker. It reads only the already retained `guest-proof.log`; no additional Docker/guest query or polling timer is added. Five offline fixture cases verify marker selection, missing/unrelated output and the bounds. The controlled 40-minute host deadline and existing cleanup are preserved.
|
||||
|
||||
## Reasons and remaining gaps
|
||||
|
||||
The existing daemon's Intel Celeron 1037U lacks AVX/AVX2; a separate diagnostic proved KVM enabled/paused state and clean exit. `CPU_MODEL=host` preserves actual instruction availability rather than advertising AVX2 through emulated Skylake. This candidate refuses a TCG or CPU-model fallback.
|
||||
@@ -29,7 +23,7 @@ Orchestration/validation remain the .NET 10 file-based app `tools/ci/MacOsNative
|
||||
~~~sh
|
||||
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --help
|
||||
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
|
||||
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --cryptex-archive /path/to/CryptexFixup-1.0.5-RELEASE.zip --noavx-archive /path/to/NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip --output /path/to/fresh/validation
|
||||
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --cryptex-archive /path/to/CryptexFixup-1.0.5-RELEASE.zip --output /path/to/fresh/validation
|
||||
~~~
|
||||
|
||||
`--validate` checks result/container contracts without Docker. With `--source` it verifies the actual Cryptex ZIP/bundle, source seams, generated OpenCore configuration and staging/checksum contracts, checks Bash syntax, then exercises four raw/zlib Recovery fixtures and twelve rejection cases with independent C# CRC32 readback. It also checks preservation of a successful resource snapshot after a later failed capture, leaving the supplied source untouched. It does not download/extract the LongQT ISO, verify a complete Apple Recovery image or execute the active-Lilu runtime checks. The ISO checksum is enforced during the later Docker build; active Lilu and EFI-copy checks execute only during container boot. The optional local Cryptex ZIP must match the release size/hash; omitting it downloads only the public 69,703-byte release. Use a fresh output directory. Dependencies are .NET 10, Git, Bash and Python 3 with its standard library; manual execution also requires the existing Linux/x64 Docker daemon and its existing KVM device.
|
||||
@@ -41,8 +35,6 @@ dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/
|
||||
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
|
||||
~~~
|
||||
|
||||
The optional `--noavx-archive` supplies the exact local upstream ZIP; omitting it downloads only the pinned 98,356-byte archive. Offline validation reads the actual generated OpenCore plist and staged executable bytes, rejects four invalid archive inputs, two missing/corrupted staging cases and five wrong `Kernel.Add` variants, then restores the valid fixture. `noavx-validation.json` records these checks. No Docker or guest is executed, and no new runner dependencies are introduced.
|
||||
|
||||
## Exact bootasset contract
|
||||
|
||||
Dockur stays pinned to `16a5b470cdd601bae8b05b02d748d7edfb36c12e`. Original Recovery patcher/staging, Dockerfile, OpenCore script and active config hashes are verified before edits. Both existing QEMU image digests remain pinned; other existing upstream downloads are observed through image identity. `source-hashes.json` includes the generated Recovery patcher, both original/replacement daemon variants and `udif_checksums.py`, staged from `tools/ci/macos-native-udif-checksums.py`. This small Python module belongs to the existing Linux UDIF runtime; C# supplies orchestration, validation fixtures and an independent CRC32 implementation.
|
||||
@@ -59,25 +51,13 @@ The [original LongQT v0.7 template](https://github.com/LongQT-sea/OpenCore-ISO/r
|
||||
|
||||
Active `/assets/config.plist` receives exactly one enabled Cryptex immediately after enabled Lilu, preserving every other kext's order. Entry: `Arch=x86_64`, `BundlePath=CryptexFixup.kext`, `ExecutablePath=Contents/MacOS/CryptexFixup`, `PlistPath=Contents/Info.plist`, `MinKernel=22.0.0`, empty `MaxKernel`. [OpenCore Kernel.Add](https://github.com/acidanthera/OpenCorePkg/blob/1.0.7/Docs/Configuration.tex) requires dependencies first; bounds are Darwin versions. Runtime rechecks order/enabled/paths/architecture/bounds and rejects unverified `/custom.plist`.
|
||||
|
||||
The additional [OCLP 2.5.1 NoAVX AVXpel archive](https://raw.githubusercontent.com/dortania/OpenCore-Legacy-Patcher/f40057a5292f4804b51bcfe78d5047c7302a6434/payloads/Kexts/Misc/NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip) is pinned to commit `f40057a5292f4804b51bcfe78d5047c7302a6434`, size 98,356 and SHA256 `b5d6319d0a1f335684a92ecf23369bc3deb776be19e92b0a40860021409d20df`. The checksum is a locally verified content pin. Only its two expected bundle files are staged; ZIP resource-fork metadata is excluded. Bundle identity `com.apple.AppleFSCompression.NoAVXFSCompressionTypeZlib`, versions `1.0.0` / `132.100.2` and `OSBundleRequired=Root` are checked before copying.
|
||||
|
||||
NoAVX follows Cryptex in `Kernel.Add`, enabled with `Arch=x86_64`, `ExecutablePath=Contents/MacOS/NoAVXFSCompressionTypeZlib`, `PlistPath=Contents/Info.plist`, `MinKernel=22.0.0` and empty `MaxKernel`. The executable name intentionally omits `-AVXpel`. [OCLP's upstream configuration](https://github.com/dortania/OpenCore-Legacy-Patcher/blob/2.5.1/payloads/Config/config.plist#L1270) selects this 12.6-based patched binary for Ventura 13.0+, rather than the older non-AVX 12.3.1 bundle limited to Darwin 21. Both overlay files enter the existing SHA256SUMS checks before and after the guest-EFI copy. OpenCore boot injection also applies to Recovery; this is no installed-APFS-only root patch. Whether it fixes this guest's hang remains an operational question.
|
||||
|
||||
No new force/beta argument is needed for actual no-AVX2 CPUs. Baseline arguments remain. Validation rejects disabling arguments, `-crypt_allow_hash_validation` (disables the APFS patch) and unexpected Cryptex force/beta overrides. Manifest/profile enter the boot signature; this candidate always rebuilds `boot.img` and accepts no old cache as evidence.
|
||||
|
||||
## Gates, privileges and cleanup
|
||||
|
||||
The Apple wrapper is byte-identical to baseline: background `/Volumes/installstate/readiness.sh` then `exec /usr/libexec/recoveryosd` under the same launchd job/PID. Source evidence does not prove Apple's executable ran.
|
||||
|
||||
The disk IPC continuation contains seven explicitly marked diagnostic blocks and limits disk enumeration to one attempt. Its disk query receives 120 seconds so the owned sample can finish while the query is still running. Validation removes only those marked blocks, including that command-budget exception, restores the former attempt condition and normalizes macOS 13 to 14 before requiring baseline SHA256 `4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3`. Two separate named version marker pairs exclude the new parser and explicitly restore the original three-line `sw_vers -productVersion` sequence for this baseline comparison. The receipt records this version-source exception, the 1,024-byte parser bound and all existing diagnostic budget exceptions. Architecture, UID, native service exits, minimum macOS version, disk size/writability/uniqueness, proof bounds and native-wait/cleanup/flush metrics remain identical. All other required native commands retain 45 seconds, UID retains 180 seconds, and outer limits remain ten minutes maximum disk readiness, 40 minutes host and 45 minutes workflow.
|
||||
|
||||
Run 4186 at `227884723a25e703ec1be39f8600eeaae2085c4f` completed native `sw_vers` successfully after 43 seconds, reporting macOS 13.6 / 22G120. The redundant `sw_vers -productVersion` then timed out after 48 seconds before any disk query. The continuation extracts exactly one `ProductVersion` field from the entire already-successful `platform` or `platform-warm` output. It requires EOF within 1,024 bytes, rejects NUL delimiters, duplicate/missing fields and malformed version suffixes, and accepts only two or three numeric version components separated by dots with native tab/space padding. The existing macOS 13 minimum remains mandatory; no native timeout is relaxed by this reuse.
|
||||
|
||||
Run 4175 at `94a70b200508d3ba295124896d923fbb785d1658` reached macOS 13.6, x86_64 and UID 0 with KVM enabled; its nine `diskutil list physical` attempts timed out. Run 4185 at `25989cf0eb7205f30ac0bb44eb279aa3b463f12c` proved the whole writable 64-GiB target as IOMedia `disk2`; it measured approximately 14 seconds for the final native process listing and 33 seconds for IOMedia. Both Apple disk jobs were running; DiskManagement's endpoint was still inactive. The former eight-second observer allowance was shorter than observed native startup, so its killed sample did not establish an IPC wait point. A missing target is ruled out for that run; service initialization, IPC or resource delays remain unresolved.
|
||||
|
||||
Before the only disk attempt the continuation records bounded `launchctl print` output for `com.apple.diskarbitrationd` and `com.apple.diskmanagementd`, plus `ioreg -r -c IOMedia -l -w 0`. Its observer starts only `/usr/bin/sample <owned-diskutil-child-pid> 3 100 -file <owned-output>`, with no preceding process list or additional service query. Three seconds at a 100-millisecond interval reduces sampling overhead. The sample has 60 seconds for startup/reporting plus the existing two-second TERM/KILL grace; its separate report is flushed into the proof alongside command output. Missing sample tooling or an already completed diskutil is reported explicitly; nonzero observation exits are logged and cannot satisfy any native gate.
|
||||
|
||||
The observer owns its command/timer PIDs and is stopped when the disk query completes or the probe is canceled. Its output enters the existing 512-KiB per-output and 4-MiB proof budgets. The hook only reads media/service/process state and writes its existing diagnostic files: it does not load, restart, erase or modify any service or disk. Bash remains necessary because Apple Recovery runs this hook before a .NET SDK is installed. The CPU, Recovery, QEMU, Apple wrapper and container profile are unchanged. These observations are prepared diagnostics, not a new successful guest or full native CI receipt.
|
||||
Readiness changes only minimum macOS 14 to 13. Validation normalizes that gate to 14 and requires baseline SHA256 `4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3`. Architecture, UID, services, disk size/writability/uniqueness, retries, proof bounds, timers and native-wait/cleanup/flush metrics remain identical. Limits stay 45 seconds per native command, 180 seconds for UID, ten minutes disk readiness, 40 minutes host and 45 minutes workflow.
|
||||
|
||||
Container profile: `KVM=Y`, `CPU_MODEL=host`, `VERSION=13`, 4-GiB guest, two guest/host CPUs, 6-GiB memory/swap and 512-MiB shared memory. Fresh anonymous `/storage` holds the 64-GiB disk; evidence reads `/storage/13/setup.dmg`. Existing resource budget checks remain.
|
||||
|
||||
@@ -85,6 +65,4 @@ Only device mapping: exactly `/dev/kvm:/dev/kvm:rw`. Inspection rejects other de
|
||||
|
||||
Evidence retains run/profile identity, source/assets, EFI staging, container/resources, macOS 13 Recovery hash, native proof/result/outcome and cleanup. `[recovery-original]` logs the exact download's size/SHA256 before modifying it, including when patch failure later deletes the source. `guest-container-resources.last-success.stdout.log` and its timestamp/hash receipt preserve the last successful resource snapshot independently of a later failed stopped-container `docker exec`. Optional final Unix HMP capture includes `info kvm`, `info status` and a bounded PPM exported from `/tmp`; capture success passes no native gate.
|
||||
|
||||
Optional 20-second resource snapshots before, during and after the guest probe retain cgroup CPU usage/throttling/pressure, memory events/pressure/statistics and host page-fault/swap counters. These observations test resource contention as a hypothesis; no resource failure is established by the existing guest timing alone. The large Recovery image hash is captured once after compatibility-profile staging is observed, with its successful receipt retained, instead of repeatedly hashing the image while collecting guest progress. Snapshot or hash observation failure cannot satisfy a native readiness gate.
|
||||
|
||||
Both cleanup paths keep exact token/label/ID checks. `docker rm --force --volumes` removes only the owned container and anonymous volume, then its exact image; no unrelated objects or pruning. Evidence stays seven days. Full native CI still needs a subsequent actual installed remote guest to build/sign helpers and pass the full suite, including five native tests without skips.
|
||||
@@ -16,8 +16,6 @@ static class NativeDiagnostic
|
||||
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
|
||||
const string CryptexUrl = "https://github.com/acidanthera/CryptexFixup/releases/download/1.0.5/CryptexFixup-1.0.5-RELEASE.zip";
|
||||
const string CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30";
|
||||
const string NoAvxUrl = "https://raw.githubusercontent.com/dortania/OpenCore-Legacy-Patcher/f40057a5292f4804b51bcfe78d5047c7302a6434/payloads/Kexts/Misc/NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip";
|
||||
const string NoAvxHash = "b5d6319d0a1f335684a92ecf23369bc3deb776be19e92b0a40860021409d20df";
|
||||
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
|
||||
const string UdifChecksumBindingHash = "6109d04619e800c483fdac363d593cd1cd69f34131d2521417334e11d41c8bfa";
|
||||
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
|
||||
@@ -59,23 +57,19 @@ static class NativeDiagnostic
|
||||
{
|
||||
if (args.Length == 0 || args.Contains("--help"))
|
||||
{
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone] [--cryptex-archive verified-release.zip] [--noavx-archive verified-upstream.zip]");
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone] [--cryptex-archive verified-release.zip]");
|
||||
return 0;
|
||||
}
|
||||
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
|
||||
if (args.Contains("--validate"))
|
||||
{
|
||||
ValidateRunnerMemoryGate();
|
||||
ValidateGuestProgress(output);
|
||||
ValidateContracts();
|
||||
if (Option(args, "--source") is { } source)
|
||||
{
|
||||
await PrepareSource(Path.GetFullPath(source), output, "validation", false, Option(args, "--cryptex-archive"), Option(args, "--noavx-archive"), CancellationToken.None);
|
||||
ValidateNoAvxStaging(output);
|
||||
ValidateNoAvxRejections(output);
|
||||
await PrepareSource(Path.GetFullPath(source), output, "validation", false, Option(args, "--cryptex-archive"), CancellationToken.None);
|
||||
await ValidateResourceRetention(output);
|
||||
await ValidateRecoveryPatch(output);
|
||||
Save(Path.Combine(output, "validation.json"), new { success = true, profile = "kvm-host-ventura-cryptex-noavx", helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))), udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))), baselineReadinessNormalized = true, readinessDiagnosticBlocksExcluded = 7, productVersionParserBlockExcluded = true, productVersionSequenceRestored = true, productVersionMaximumBytes = 1024, nativeProductVersionCommandRemoved = true, diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 120, diskSampleLimitSeconds = 60, diskSampleDurationSeconds = 3, diskSampleIntervalMilliseconds = 100, resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12, independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cryptexArchiveVerified = true, noAvxArchiveVerified = true, configurationAndStagingContractsVerified = true, templateIsoDownloaded = false, activeLiluRuntimeChecked = false, sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow });
|
||||
Save(Path.Combine(output, "validation.json"), new { success = true, profile = "kvm-host-ventura-cryptex", helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))), udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))), baselineReadinessNormalized = true, resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12, independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cryptexArchiveVerified = true, configurationAndStagingContractsVerified = true, templateIsoDownloaded = false, activeLiluRuntimeChecked = false, sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow });
|
||||
}
|
||||
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
|
||||
return 0;
|
||||
@@ -104,7 +98,7 @@ static class NativeDiagnostic
|
||||
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
|
||||
ValidateContracts();
|
||||
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
|
||||
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex-noavx", causalSingleVariableTest = true, comparisonBaselineCommit = "720a43158c17253b65eaadc6fcce6d27f52e373e", kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", noAvxBaseVersion = "12.6", noAvxSha256 = NoAvxHash, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
|
||||
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex", causalSingleVariableTest = false, kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
|
||||
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
|
||||
using (var document = JsonDocument.Parse(info.Output))
|
||||
{
|
||||
@@ -115,14 +109,15 @@ static class NativeDiagnostic
|
||||
throw new InvalidOperationException("Existing Docker resources cannot fit this bounded 2-CPU/6-GiB diagnostic; no infrastructure change was requested.");
|
||||
}
|
||||
await Command("sh", ["-c", "cat /proc/meminfo; printf '\n[cgroup]\n'; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true; printf '\n[workspace disk]\n'; df -Pk ."], output, "runner-resources", deadline.Token);
|
||||
if (!HasAvailableGuestMemory(File.ReadAllText("/proc/meminfo")))
|
||||
throw new InvalidOperationException("Existing runner memory cannot fit the 4-GiB guest plus its 512-MiB QEMU overhead budget; no infrastructure change was requested.");
|
||||
var available = System.Text.RegularExpressions.Regex.Match(File.ReadAllText("/proc/meminfo"), @"(?m)^MemAvailable:\s+(\d+) kB$");
|
||||
if (!available.Success || long.Parse(available.Groups[1].Value) < 5L * 1024 * 1024)
|
||||
throw new InvalidOperationException("Existing runner memory has less than the 5-GiB available diagnostic budget; no infrastructure change was requested.");
|
||||
var source = Path.Combine(work, "dockur");
|
||||
await Command("git", ["clone", "--no-checkout", "https://github.com/dockur/macos.git", source], output, "dockur-clone", deadline.Token);
|
||||
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
|
||||
var actualCommit = (await Command("git", ["-C", source, "rev-parse", "HEAD"], output, "dockur-commit", deadline.Token)).Output.Trim();
|
||||
if (actualCommit != DockurCommit) throw new InvalidOperationException("Dockur source pin mismatch.");
|
||||
await PrepareSource(source, output, token, true, Option(args, "--cryptex-archive"), Option(args, "--noavx-archive"), deadline.Token);
|
||||
await PrepareSource(source, output, token, true, Option(args, "--cryptex-archive"), deadline.Token);
|
||||
await Command("docker", ["build", "--platform", "linux/amd64", "--label", OwnerLabel + "=" + token, "--tag", state.ImageTag, source], output, "docker-build", deadline.Token, echo: true);
|
||||
var imageInspect = await Command("docker", ["image", "inspect", state.ImageTag], output, "image-inspect", deadline.Token);
|
||||
using (var image = JsonDocument.Parse(imageInspect.Output))
|
||||
@@ -136,21 +131,13 @@ static class NativeDiagnostic
|
||||
await Command("docker", ["inspect", id], output, "container-created", deadline.Token);
|
||||
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
|
||||
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
|
||||
await CapturePressure(id, output, "before", deadline.Token);
|
||||
Console.WriteLine("The owned restricted KVM/host-CPU macOS 13 compatibility guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run. The additional NoAVX boot kext is the only guest variable against KVM baseline 720a431.");
|
||||
Console.WriteLine("The owned restricted KVM/host-CPU macOS 13 compatibility guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run. This is not a single-variable causal test.");
|
||||
var recoveryStarted = Stopwatch.StartNew();
|
||||
var heartbeat = Stopwatch.StartNew();
|
||||
var diskPressureCaptured = false;
|
||||
while (true)
|
||||
{
|
||||
deadline.Token.ThrowIfCancellationRequested();
|
||||
await CaptureGuest(id, output, deadline.Token);
|
||||
var proofPath = Path.Combine(output, "guest-proof.log");
|
||||
if (!diskPressureCaptured && File.Exists(proofPath) && File.ReadAllText(proofPath).Contains("[proof-start] disks", StringComparison.Ordinal))
|
||||
{
|
||||
diskPressureCaptured = true;
|
||||
await CapturePressure(id, output, "during", deadline.Token);
|
||||
}
|
||||
var resultPath = Path.Combine(output, "guest-result.json");
|
||||
if (File.Exists(resultPath))
|
||||
{
|
||||
@@ -164,8 +151,7 @@ static class NativeDiagnostic
|
||||
if (running.Output.Trim() != "true") throw new InvalidOperationException("Guest container exited before a native readiness result.");
|
||||
if (heartbeat.Elapsed >= TimeSpan.FromSeconds(60))
|
||||
{
|
||||
Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1}/40 minutes; container=running; readiness=pending");
|
||||
foreach (var progress in LastGuestProgress(proofPath)) Console.WriteLine("[native-diagnostic] guest-progress=" + progress);
|
||||
Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1} minutes; container=running; readiness=pending");
|
||||
heartbeat.Restart();
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(20), deadline.Token);
|
||||
@@ -181,7 +167,6 @@ static class NativeDiagnostic
|
||||
Console.CancelKeyPress -= cancelHandler;
|
||||
using var captureDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
try { await CaptureGuest(state.ContainerId ?? state.ContainerName, output, captureDeadline.Token, true, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
|
||||
await CapturePressure(state.ContainerId ?? state.ContainerName, output, "after", captureDeadline.Token);
|
||||
try { PrintGuestProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Native proof output: " + exception.Message); }
|
||||
var clean = await Cleanup(output);
|
||||
if (!clean) { outcome = "failed"; error = (error ?? "") + " Owned-resource cleanup failed; inspect cleanup evidence."; }
|
||||
@@ -199,11 +184,9 @@ static class NativeDiagnostic
|
||||
static void ValidateContracts()
|
||||
{
|
||||
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
|
||||
var baseline = NormalizeReadinessDiagnostics(readiness);
|
||||
baseline = ReplaceOnce(baseline, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
|
||||
baseline = ReplaceOnce(baseline, "while (( attempt < 1 && SECONDS - readiness_start < 600 )); do", "while (( SECONDS - readiness_start < 600 )); do");
|
||||
var baseline = ReplaceOnce(readiness, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
|
||||
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
|
||||
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, the successful sw_vers version parser/sequence, the macOS minimum and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
|
||||
throw new InvalidOperationException("Compatibility readiness may change only the baseline's macOS minimum to 13; identity, services, disk and limits must remain identical.");
|
||||
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
|
||||
throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper.");
|
||||
if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist.");
|
||||
@@ -231,39 +214,7 @@ static class NativeDiagnostic
|
||||
}
|
||||
}
|
||||
|
||||
static string NormalizeReadinessDiagnostics(string source)
|
||||
{
|
||||
const string start = "# BEGIN disk IPC diagnostic\n";
|
||||
const string end = "# END disk IPC diagnostic\n";
|
||||
var blocks = 0;
|
||||
while (source.IndexOf(start, StringComparison.Ordinal) is var from && from >= 0)
|
||||
{
|
||||
var to = source.IndexOf(end, from + start.Length, StringComparison.Ordinal);
|
||||
if (to < 0 || source.IndexOf(start, from + start.Length, to - from - start.Length, StringComparison.Ordinal) >= 0)
|
||||
throw new InvalidOperationException("Readiness diagnostic blocks are unbalanced or nested.");
|
||||
source = source.Remove(from, to + end.Length - from);
|
||||
blocks++;
|
||||
}
|
||||
if (blocks != 7 || source.Contains(end, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("Readiness must contain exactly seven explicit disk IPC diagnostic blocks.");
|
||||
source = RestoreVersionBlock(source, "successful sw_vers version parser", "");
|
||||
source = RestoreVersionBlock(source, "successful sw_vers version extraction", "run_command version /usr/bin/sw_vers -productVersion\n(( LAST_EXIT == 0 )) || fail_probe product_version_failed\nread_scalar || fail_probe product_version_invalid\n");
|
||||
return source;
|
||||
}
|
||||
|
||||
static string RestoreVersionBlock(string source, string name, string originalSequence)
|
||||
{
|
||||
var start = "# BEGIN " + name + "\n";
|
||||
var end = "# END " + name + "\n";
|
||||
if (source.Split(start, StringSplitOptions.None).Length != 2 || source.Split(end, StringSplitOptions.None).Length != 2)
|
||||
throw new InvalidOperationException("Readiness requires exactly one named version marker pair: " + name);
|
||||
var from = source.IndexOf(start, StringComparison.Ordinal);
|
||||
var to = source.IndexOf(end, StringComparison.Ordinal);
|
||||
if (to < from + start.Length) throw new InvalidOperationException("Readiness version markers are reversed: " + name);
|
||||
return source.Remove(from, to + end.Length - from).Insert(from, originalSequence);
|
||||
}
|
||||
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, string? cryptexArchive, string? noAvxArchive, CancellationToken cancellation)
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, string? cryptexArchive, CancellationToken cancellation)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
|
||||
@@ -281,7 +232,7 @@ static class NativeDiagnostic
|
||||
var dockerfile = ReplaceOnce(File.ReadAllText(dockerPath), "FROM scratch AS base\nCOPY --from=qemux/qemu:7.50 --exclude=usr/bin/qemu-system-x86_64 / /\n", "FROM qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df AS base\n");
|
||||
dockerfile = ReplaceAllExact(dockerfile, "--from=qemux/qemu-macos:latest ", "--from=qemux/qemu-macos:latest@sha256:af64297171228f27d5f616249e18f6ad5e2fbc79c1cc517252521e8bcd8eadaa ", 2);
|
||||
dockerfile = ReplaceOnce(dockerfile, "ADD $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso", "ADD --checksum=sha256:" + OpenCoreTemplateHash + " $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso");
|
||||
var compatibility = await PrepareCompatibility(source, output, cryptexArchive, noAvxArchive, cancellation);
|
||||
var compatibility = await PrepareCompatibility(source, output, cryptexArchive, cancellation);
|
||||
var entryPath = Path.Combine(source, "src/entry.sh");
|
||||
var entry = ReplaceOnce(File.ReadAllText(entryPath), "set -Eeuo pipefail\n", "set -Eeuo pipefail\n\n# Diagnostic budget: inspect existing Docker storage before Recovery download/boot.\ndf -Pk /storage\nfree_kib=$(df -Pk /storage | awk 'NR==2 {print $4}')\n[[ \"$free_kib\" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || { echo 'Existing Docker storage has less than the 8-GiB diagnostic budget.' >&2; exit 1; }\n");
|
||||
entry = ReplaceOnce(entry, ". init.sh # Initialize system\n", ". init.sh # Initialize system\n# Fail before Apple downloads if the existing daemon cannot retain this profile.\nenabled \"$KVM\" && [[ \"$CPU_MODEL\" == host && \"$VERSION\" == 13 ]] && grep -Eq '^vendor_id[[:space:]]*:[[:space:]]*GenuineIntel$' /proc/cpuinfo || { error 'Compatibility probe requires existing Intel KVM and the exact host/13 profile.'; exit 1; }\n");
|
||||
@@ -366,7 +317,7 @@ static class NativeDiagnostic
|
||||
return string.Join("\n", lines.Select(line => new string(' ', spaces) + (line.Length > 0 ? line[common..] : "")));
|
||||
}
|
||||
|
||||
static async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, string? noAvxArchivePath, CancellationToken cancellation)
|
||||
static async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, CancellationToken cancellation)
|
||||
{
|
||||
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
|
||||
var config = File.ReadAllText(Path.Combine(source, "assets", "config.plist"));
|
||||
@@ -397,109 +348,24 @@ static class NativeDiagnostic
|
||||
var info = XDocument.Load(Path.Combine(assets, required[0])).Root!.Element("dict")!;
|
||||
if (PlistValue(info, "CFBundleIdentifier").Value != "com.khronokernel.CryptexFixup" || PlistValue(info, "CFBundleVersion").Value != "1.0.5" || PlistValue(info, "CFBundleExecutable").Value != "CryptexFixup" || PlistValue(PlistValue(info, "OSBundleLibraries"), "as.vit9696.Lilu").Value != "1.4.7") throw new InvalidOperationException("Cryptex bundle identity/version/Lilu dependency mismatch.");
|
||||
var fileHashes = required.ToDictionary(name => name, name => Hash(File.ReadAllBytes(Path.Combine(assets, name))));
|
||||
byte[] noAvxBytes;
|
||||
if (noAvxArchivePath is not null) noAvxBytes = await File.ReadAllBytesAsync(noAvxArchivePath, cancellation);
|
||||
else
|
||||
{
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30), MaxResponseContentBufferSize = 2 * 1024 * 1024 };
|
||||
noAvxBytes = await client.GetByteArrayAsync(NoAvxUrl, cancellation);
|
||||
}
|
||||
var noAvxFiles = ReadNoAvxArchive(noAvxBytes);
|
||||
File.WriteAllBytes(Path.Combine(output, "NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip"), noAvxBytes);
|
||||
foreach (var (name, content) in noAvxFiles)
|
||||
{
|
||||
var destination = Path.Combine(assets, name);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.WriteAllBytes(destination, content);
|
||||
fileHashes.Add(name, Hash(content));
|
||||
}
|
||||
File.WriteAllText(Path.Combine(assets, "SHA256SUMS"), string.Concat(fileHashes.Select(pair => pair.Value + " " + pair.Key + "\n")), new UTF8Encoding(false));
|
||||
Save(Path.Combine(output, "compatibility-boot-assets.json"), new { cryptexUrl = CryptexUrl, cryptexSha256 = CryptexHash, cryptexBytes = bytes.Length, compatibilityFiles = fileHashes, noAvxUrl = NoAvxUrl, noAvxSha256 = NoAvxHash, noAvxBytes = noAvxBytes.Length, noAvxBaseVersion = "12.6", noAvxMinimumDarwin = "22.0.0", noAvxRequired = "Root", templateUrl = "https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso", templateSha256 = OpenCoreTemplateHash, templateBytes = 15884288, liluVersion = "1.7.1", liluBinarySha256 = "0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52", liluBinaryBytes = 526984, liluInfoSha256 = "fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a", causalSingleVariableTest = true, comparisonBaselineCommit = "720a431", changedBootAsset = "NoAVXFSCompressionTypeZlib-AVXpel.kext" });
|
||||
Save(Path.Combine(output, "compatibility-boot-assets.json"), new { cryptexUrl = CryptexUrl, cryptexSha256 = CryptexHash, cryptexBytes = bytes.Length, cryptexFiles = fileHashes, templateUrl = "https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso", templateSha256 = OpenCoreTemplateHash, templateBytes = 15884288, liluVersion = "1.7.1", liluBinarySha256 = "0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52", liluBinaryBytes = 526984, liluInfoSha256 = "fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a", causalSingleVariableTest = false });
|
||||
var document = XDocument.Parse(config, LoadOptions.PreserveWhitespace);
|
||||
var add = PlistValue(PlistValue(document.Root!.Element("dict")!, "Kernel"), "Add");
|
||||
var expected = new[] { "Lilu.kext", "VMHide.kext", "VirtualSMC.kext", "WhateverGreen.kext", "VoodooPS2Controller.kext", "VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext", "AppleMCEReporterDisabler.kext" };
|
||||
if (!add.Elements("dict").Select(dict => PlistValue(dict, "BundlePath").Value).SequenceEqual(expected) || add.Elements("dict").Any(dict => PlistValue(dict, "Enabled").Name != "true")) throw new InvalidOperationException("Pinned Kernel.Add order/enabled contract mismatch.");
|
||||
var cryptex = XElement.Parse("<dict><key>Arch</key><string>x86_64</string><key>BundlePath</key><string>CryptexFixup.kext</string><key>Comment</key><string>Official CryptexFixup 1.0.5; owned compatibility guest only</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/CryptexFixup</string><key>MaxKernel</key><string></string><key>MinKernel</key><string>22.0.0</string><key>PlistPath</key><string>Contents/Info.plist</string></dict>");
|
||||
add.Elements("dict").First().AddAfterSelf(cryptex);
|
||||
cryptex.AddAfterSelf(XElement.Parse("<dict><key>Arch</key><string>x86_64</string><key>BundlePath</key><string>NoAVXFSCompressionTypeZlib-AVXpel.kext</string><key>Comment</key><string>OCLP 2.5.1 AVXpel 12.6; Ventura filesystem compression hypothesis</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/NoAVXFSCompressionTypeZlib</string><key>MaxKernel</key><string></string><key>MinKernel</key><string>22.0.0</string><key>PlistPath</key><string>Contents/Info.plist</string></dict>"));
|
||||
var bootArguments = PlistValue(PlistValue(PlistValue(PlistValue(document.Root.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82"), "boot-args").Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (bootArguments.Intersect(new[] { "-cryptoff", "-liluoff", "-crypt_allow_hash_validation", "-crypt_force_avx", "-cryptbeta", "-lilubetaall" }).Any()) throw new InvalidOperationException("Unexpected Cryptex/Lilu disabling or forcing boot argument.");
|
||||
boot = ReplaceOnce(boot, " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n", " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n" + CompatibilityStaging + "\n");
|
||||
boot = ReplaceOnce(boot, " PLIST=\"/assets/config.plist\"\n", " [ ! -e /custom.plist ] || { error 'Compatibility profile refuses an unverified custom OpenCore config!'; exit 12; }\n PLIST=\"/assets/config.plist\"\n");
|
||||
boot = ReplaceOnce(boot, " checkOpenCoreConfig\n addVmHideKext\n", " checkOpenCoreConfig\n" + CompatibilityConfigCheck + "\n addVmHideKext\n");
|
||||
boot = ReplaceOnce(boot, " if [ -s \"$target\" ] && [ \"$previous\" = \"$current\" ]; then\n IMG=\"$target\"\n return 0\n fi\n", " # This owned compatibility probe always rebuilds; never trust a cached boot.img.\n");
|
||||
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"COMPATIBILITY=kvm-host-ventura-cryptex-noavx\"\n sha256sum /assets/native-compatibility/SHA256SUMS\n");
|
||||
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"COMPATIBILITY=kvm-host-ventura-cryptex\"\n sha256sum /assets/native-compatibility/SHA256SUMS\n");
|
||||
return (boot, document.ToString(), assets);
|
||||
}
|
||||
|
||||
static bool HasAvailableGuestMemory(string meminfo)
|
||||
{
|
||||
var available = System.Text.RegularExpressions.Regex.Match(meminfo, @"(?m)^MemAvailable:\s+(\d+) kB$");
|
||||
return available.Success && long.TryParse(available.Groups[1].Value, out var kib) && kib >= 4L * 1024 * 1024 + 512L * 1024;
|
||||
}
|
||||
|
||||
static string[] LastGuestProgress(string path)
|
||||
{
|
||||
if (!File.Exists(path)) return [];
|
||||
var markers = new[] { "[proof-start]", "[proof-done]", "[proof-native-wait]", "[proof-result]", "[native-version]" };
|
||||
return File.ReadLines(path).Where(line => markers.Any(marker => line.StartsWith(marker, StringComparison.Ordinal)))
|
||||
.TakeLast(2).Select(line => line[..Math.Min(line.Length, 256)]).ToArray();
|
||||
}
|
||||
|
||||
static void ValidateGuestProgress(string output)
|
||||
{
|
||||
var fixture = Path.Combine(output, "validation-guest-progress");
|
||||
Directory.CreateDirectory(fixture);
|
||||
var path = Path.Combine(fixture, "guest-proof.log");
|
||||
File.Delete(path);
|
||||
if (LastGuestProgress(path).Length != 0) throw new InvalidOperationException("Missing guest progress was fabricated.");
|
||||
File.WriteAllText(path, "[proof-start] platform child=12\nignored native output\n[proof-native-wait] platform exit=0\n[proof-start] uid child=13\n", new UTF8Encoding(false));
|
||||
if (!LastGuestProgress(path).SequenceEqual(new[] { "[proof-native-wait] platform exit=0", "[proof-start] uid child=13" })) throw new InvalidOperationException("Heartbeat must show the last two captured native progress markers.");
|
||||
File.WriteAllText(path, "[proof-done] uid\n[native-version] 13.6\n[proof-result] true: readiness\n", new UTF8Encoding(false));
|
||||
if (!LastGuestProgress(path).SequenceEqual(new[] { "[native-version] 13.6", "[proof-result] true: readiness" })) throw new InvalidOperationException("Heartbeat lost native version/result progress.");
|
||||
File.WriteAllText(path, "[proof-start] " + new string('x', 1024) + "\n", new UTF8Encoding(false));
|
||||
if (LastGuestProgress(path).Single().Length != 256) throw new InvalidOperationException("Heartbeat progress line exceeded its output bound.");
|
||||
File.WriteAllText(path, "unrelated output\n", new UTF8Encoding(false));
|
||||
if (LastGuestProgress(path).Length != 0) throw new InvalidOperationException("Heartbeat selected unrelated guest output.");
|
||||
Save(Path.Combine(fixture, "receipt.json"), new { success = true, fixtureCases = 5, maximumLines = 2, maximumLineCharacters = 256, existingProofOnly = true, dockerExecuted = false, guestExecuted = false });
|
||||
}
|
||||
|
||||
static void ValidateRunnerMemoryGate()
|
||||
{
|
||||
// Run 4204: 4-GiB guest plus 512-MiB QEMU overhead fits its captured available memory.
|
||||
var cases = new[]
|
||||
{
|
||||
("captured-run4204", "MemTotal: 16281732 kB\nMemAvailable: 5138696 kB\n", true),
|
||||
("below-guest-plus-overhead", "MemAvailable: 4194304 kB\n", false),
|
||||
("missing", "MemTotal: 16281732 kB\n", false),
|
||||
("invalid", "MemAvailable: unavailable kB\n", false)
|
||||
};
|
||||
foreach (var (name, meminfo, expected) in cases)
|
||||
if (HasAvailableGuestMemory(meminfo) != expected)
|
||||
throw new InvalidOperationException("Existing runner memory admission failed: " + name);
|
||||
}
|
||||
|
||||
static Dictionary<string, byte[]> ReadNoAvxArchive(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 98356 || Hash(bytes) != NoAvxHash) throw new InvalidOperationException("Pinned OCLP NoAVX archive size/hash mismatch.");
|
||||
using var archive = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read);
|
||||
var required = new[] { "NoAVXFSCompressionTypeZlib-AVXpel.kext/Contents/Info.plist", "NoAVXFSCompressionTypeZlib-AVXpel.kext/Contents/MacOS/NoAVXFSCompressionTypeZlib" };
|
||||
var entries = archive.Entries.Where(entry => entry.FullName.StartsWith("NoAVXFSCompressionTypeZlib-AVXpel.kext/", StringComparison.Ordinal) && !entry.FullName.EndsWith('/')).ToArray();
|
||||
if (entries.Length != 2 || required.Any(name => entries.Count(entry => entry.FullName == name) != 1) || entries.Any(entry => entry.Length <= 0 || entry.Length > 1024 * 1024)) throw new InvalidOperationException("NoAVX archive layout/size mismatch.");
|
||||
var files = new Dictionary<string, byte[]>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
using var input = entry.Open();
|
||||
using var content = new MemoryStream();
|
||||
input.CopyTo(content);
|
||||
if (content.Length != entry.Length) throw new InvalidOperationException("NoAVX archive entry length mismatch.");
|
||||
files.Add(entry.FullName, content.ToArray());
|
||||
}
|
||||
var info = XDocument.Parse(Encoding.UTF8.GetString(files[required[0]])).Root!.Element("dict")!;
|
||||
if (PlistValue(info, "CFBundleIdentifier").Value != "com.apple.AppleFSCompression.NoAVXFSCompressionTypeZlib" || PlistValue(info, "CFBundleExecutable").Value != "NoAVXFSCompressionTypeZlib" || PlistValue(info, "CFBundleVersion").Value != "1.0.0" || PlistValue(info, "CFBundleShortVersionString").Value != "132.100.2" || PlistValue(info, "OSBundleRequired").Value != "Root") throw new InvalidOperationException("NoAVX bundle identity/version/root requirement mismatch.");
|
||||
return files;
|
||||
}
|
||||
|
||||
static XElement PlistValue(XElement dictionary, string key)
|
||||
{
|
||||
var keys = dictionary.Elements("key").Where(element => element.Value == key).ToArray();
|
||||
@@ -507,59 +373,6 @@ static class NativeDiagnostic
|
||||
return value;
|
||||
}
|
||||
|
||||
static void ValidateNoAvxStaging(string output)
|
||||
{
|
||||
var root = Path.Combine(output, "compatibility-assets", "NoAVXFSCompressionTypeZlib-AVXpel.kext", "Contents");
|
||||
if (!File.Exists(Path.Combine(root, "Info.plist")) || !File.Exists(Path.Combine(root, "MacOS", "NoAVXFSCompressionTypeZlib")))
|
||||
throw new InvalidOperationException("Offline validation requires the staged NoAVX bundle, with its upstream executable path.");
|
||||
var files = ReadNoAvxArchive(File.ReadAllBytes(Path.Combine(output, "NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip")));
|
||||
foreach (var (name, content) in files)
|
||||
if (!File.ReadAllBytes(Path.Combine(output, "compatibility-assets", name)).SequenceEqual(content)) throw new InvalidOperationException("Staged NoAVX bytes differ from the pinned archive.");
|
||||
var document = XDocument.Load(Path.Combine(output, "opencore-config.plist"));
|
||||
ValidateNoAvxConfig(document);
|
||||
}
|
||||
|
||||
static void ValidateNoAvxConfig(XDocument document)
|
||||
{
|
||||
var add = PlistValue(PlistValue(document.Root!.Element("dict")!, "Kernel"), "Add");
|
||||
var entries = add.Elements("dict").ToArray();
|
||||
var expected = new[] { "Lilu.kext", "CryptexFixup.kext", "NoAVXFSCompressionTypeZlib-AVXpel.kext", "VMHide.kext", "VirtualSMC.kext", "WhateverGreen.kext", "VoodooPS2Controller.kext", "VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext", "AppleMCEReporterDisabler.kext" };
|
||||
if (!entries.Select(dict => PlistValue(dict, "BundlePath").Value).SequenceEqual(expected) || entries.Any(dict => PlistValue(dict, "Enabled").Name != "true"))
|
||||
throw new InvalidOperationException("Actual OpenCore Kernel.Add order or enabled contract mismatch.");
|
||||
var noAvx = entries[2];
|
||||
if (PlistValue(noAvx, "Arch").Value != "x86_64" || PlistValue(noAvx, "ExecutablePath").Value != "Contents/MacOS/NoAVXFSCompressionTypeZlib" || PlistValue(noAvx, "PlistPath").Value != "Contents/Info.plist" || PlistValue(noAvx, "MinKernel").Value != "22.0.0" || PlistValue(noAvx, "MaxKernel").Value != "")
|
||||
throw new InvalidOperationException("Actual NoAVX Kernel.Add paths, architecture or Darwin bounds mismatch.");
|
||||
}
|
||||
|
||||
static void ValidateNoAvxRejections(string output)
|
||||
{
|
||||
static void Reject(Action validation, string name)
|
||||
{
|
||||
try { validation(); } catch (InvalidOperationException) { return; }
|
||||
throw new InvalidOperationException("NoAVX validator accepted invalid " + name);
|
||||
}
|
||||
var bytes = File.ReadAllBytes(Path.Combine(output, "NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip"));
|
||||
var corrupt = (byte[])bytes.Clone();
|
||||
corrupt[corrupt.Length / 2] ^= 1;
|
||||
foreach (var invalid in new[] { Array.Empty<byte>(), bytes[..^1], bytes.Concat(new byte[] { 0 }).ToArray(), corrupt }) Reject(() => ReadNoAvxArchive(invalid), "archive size/hash");
|
||||
var staged = Path.Combine(output, "compatibility-assets", "NoAVXFSCompressionTypeZlib-AVXpel.kext", "Contents", "MacOS", "NoAVXFSCompressionTypeZlib");
|
||||
var original = File.ReadAllBytes(staged);
|
||||
try
|
||||
{
|
||||
File.Delete(staged);
|
||||
Reject(() => ValidateNoAvxStaging(output), "missing staging executable");
|
||||
var changed = (byte[])original.Clone();
|
||||
changed[0] ^= 1;
|
||||
File.WriteAllBytes(staged, changed);
|
||||
Reject(() => ValidateNoAvxStaging(output), "changed staging executable");
|
||||
}
|
||||
finally { File.WriteAllBytes(staged, original); }
|
||||
var config = File.ReadAllText(Path.Combine(output, "opencore-config.plist"));
|
||||
foreach (var invalid in new[] { config.Replace("NoAVXFSCompressionTypeZlib-AVXpel.kext", "Wrong.kext", StringComparison.Ordinal), config.Replace("Contents/MacOS/NoAVXFSCompressionTypeZlib", "Contents/MacOS/NoAVXFSCompressionTypeZlib-AVXpel", StringComparison.Ordinal), config.Replace("22.0.0", "21.0.0", StringComparison.Ordinal), config.Replace("<true", "<false", StringComparison.Ordinal), config.Replace("<string>x86_64</string>", "<string>arm64</string>", StringComparison.Ordinal) }) Reject(() => ValidateNoAvxConfig(XDocument.Parse(invalid)), "Kernel.Add");
|
||||
ValidateNoAvxStaging(output);
|
||||
Save(Path.Combine(output, "noavx-validation.json"), new { success = true, archiveNegativeCases = 4, stagingNegativeCases = 2, kernelAddNegativeCases = 5, archiveSha256 = NoAvxHash, actualStagedBytesVerified = true, actualGeneratedKernelAddVerified = true, dockerExecuted = false, guestExecuted = false });
|
||||
}
|
||||
|
||||
const string CompatibilityStaging = """
|
||||
# Only the freshly extracted, owned guest EFI is changed; never the host.
|
||||
local lilu="$EFI_DIR/OC/Kexts/Lilu.kext/Contents"
|
||||
@@ -568,29 +381,23 @@ static class NativeDiagnostic
|
||||
0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52 "$lilu/MacOS/Lilu" | sha256sum -c - || { error "Pinned active Lilu files mismatch!"; exit 12; }
|
||||
[ "$(xmlstarlet sel -T -t -v '/plist/dict/key[.="CFBundleVersion"]/following-sibling::string[1]' "$lilu/Info.plist")" = 1.7.1 ] || { error "Active Lilu version mismatch!"; exit 12; }
|
||||
[ ! -e "$EFI_DIR/OC/Kexts/CryptexFixup.kext" ] || { error "Unexpected pre-existing Cryptex kext!"; exit 12; }
|
||||
[ ! -e "$EFI_DIR/OC/Kexts/NoAVXFSCompressionTypeZlib-AVXpel.kext" ] || { error "Unexpected pre-existing NoAVX kext!"; exit 12; }
|
||||
(cd /assets/native-compatibility && sha256sum -c SHA256SUMS) || { error "Pinned Cryptex staging files mismatch!"; exit 12; }
|
||||
cp -a /assets/native-compatibility/CryptexFixup.kext "$EFI_DIR/OC/Kexts/"
|
||||
cp -a /assets/native-compatibility/NoAVXFSCompressionTypeZlib-AVXpel.kext "$EFI_DIR/OC/Kexts/"
|
||||
(cd "$EFI_DIR/OC/Kexts" && sha256sum -c /assets/native-compatibility/SHA256SUMS) || { error "Active Cryptex copy mismatch!"; exit 12; }
|
||||
info "[compatibility-boot] Lilu=1.7.1 CryptexFixup=1.0.5 NoAVX=AVXpel-12.6 files=verified; guest injection and Recovery readiness remain unproved"
|
||||
info "[compatibility-boot] Lilu=1.7.1 CryptexFixup=1.0.5 files=verified; guest injection and Recovery readiness remain unproved"
|
||||
""";
|
||||
|
||||
const string CompatibilityConfigCheck = """
|
||||
local kernel='/plist/dict/key[.="Kernel"]/following-sibling::dict[1]/key[.="Add"]/following-sibling::array[1]'
|
||||
local actual expected
|
||||
actual=$(xmlstarlet sel -T -t -m "$kernel/dict" -v 'key[.="BundlePath"]/following-sibling::string[1]' -n "$CFG") || exit 12
|
||||
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext NoAVXFSCompressionTypeZlib-AVXpel.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
|
||||
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
|
||||
[ "$actual" = "$expected" ] || { error "Active Kernel.Add order mismatch!"; exit 12; }
|
||||
[ "$(xmlstarlet sel -T -t -v "name($kernel/dict[1]/key[.='Enabled']/following-sibling::*[1])" -v "name($kernel/dict[2]/key[.='Enabled']/following-sibling::*[1])" "$CFG")" = truetrue ] || { error "Active Lilu/Cryptex must both be enabled!"; exit 12; }
|
||||
actual=$(xmlstarlet sel -T -t -m "$kernel/dict[2]" -v 'key[.="Arch"]/following-sibling::string[1]' -n -v 'key[.="ExecutablePath"]/following-sibling::string[1]' -n -v 'key[.="PlistPath"]/following-sibling::string[1]' -n -v 'key[.="MinKernel"]/following-sibling::string[1]' -n -v 'key[.="MaxKernel"]/following-sibling::string[1]' "$CFG") || exit 12
|
||||
expected=$(printf '%s\n' x86_64 Contents/MacOS/CryptexFixup Contents/Info.plist 22.0.0 '')
|
||||
[ "$actual" = "$expected" ] || { error "Active Cryptex Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
|
||||
[ "$(xmlstarlet sel -T -t -v "name($kernel/dict[3]/key[.='Enabled']/following-sibling::*[1])" "$CFG")" = true ] || { error "Active NoAVX must be enabled!"; exit 12; }
|
||||
actual=$(xmlstarlet sel -T -t -m "$kernel/dict[3]" -v 'key[.="Arch"]/following-sibling::string[1]' -n -v 'key[.="ExecutablePath"]/following-sibling::string[1]' -n -v 'key[.="PlistPath"]/following-sibling::string[1]' -n -v 'key[.="MinKernel"]/following-sibling::string[1]' -n -v 'key[.="MaxKernel"]/following-sibling::string[1]' "$CFG") || exit 12
|
||||
expected=$(printf '%s\n' x86_64 Contents/MacOS/NoAVXFSCompressionTypeZlib Contents/Info.plist 22.0.0 '')
|
||||
[ "$actual" = "$expected" ] || { error "Active NoAVX Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
|
||||
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup,NoAVX before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
|
||||
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
|
||||
""";
|
||||
|
||||
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
|
||||
@@ -650,40 +457,7 @@ static class NativeDiagnostic
|
||||
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
|
||||
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
|
||||
}
|
||||
// The immutable Recovery image is complete only after this staging marker.
|
||||
// Hash it once instead of rereading 710 MB on every twenty-second poll.
|
||||
if (logs.Output.Contains("[compatibility-profile] accelerator=kvm", StringComparison.Ordinal)
|
||||
&& !File.Exists(Path.Combine(output, "guest-container-resources.last-success.json")))
|
||||
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test -f /storage/13/setup.dmg && sha256sum /storage/13/setup.dmg || exit 1; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false, retainSuccessful: true);
|
||||
}
|
||||
|
||||
static async Task CapturePressure(string id, string output, string phase, CancellationToken cancellation)
|
||||
{
|
||||
using var snapshotDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
snapshotDeadline.CancelAfter(TimeSpan.FromSeconds(20));
|
||||
const string snapshot = """
|
||||
printf '[snapshot UTC]\n'; date -u '+%Y-%m-%dT%H:%M:%SZ'
|
||||
for path in /proc/meminfo /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
|
||||
/sys/fs/cgroup/cpu.max /sys/fs/cgroup/cpu.stat /sys/fs/cgroup/cpu.pressure \
|
||||
/sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.peak \
|
||||
/sys/fs/cgroup/memory.events /sys/fs/cgroup/memory.stat /sys/fs/cgroup/memory.pressure \
|
||||
/sys/fs/cgroup/memory.swap.current; do
|
||||
printf '\n[%s]\n' "$path"
|
||||
if [ -r "$path" ]; then cat "$path"; else printf 'unavailable\n'; fi
|
||||
done
|
||||
printf '\n[host paging counters]\n'
|
||||
awk '/^(pgmajfault|pswpin|pswpout) / {print}' /proc/vmstat
|
||||
""";
|
||||
try
|
||||
{
|
||||
await Command("docker", ["exec", id, "sh", "-c", snapshot], output, "capture-pressure-" + phase, snapshotDeadline.Token, requireSuccess: false, retainSuccessful: true);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Optional evidence must not replace the guest outcome or prevent cleanup.
|
||||
try { Save(Path.Combine(output, "capture-pressure-" + phase + ".unavailable.json"), new { phase, error = exception.Message, capturedUtc = DateTimeOffset.UtcNow }); }
|
||||
catch (Exception evidenceError) { Console.Error.WriteLine("Optional pressure evidence: " + evidenceError.Message); }
|
||||
}
|
||||
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test ! -f /storage/13/setup.dmg || sha256sum /storage/13/setup.dmg; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false, retainSuccessful: true);
|
||||
}
|
||||
|
||||
static async Task CaptureMonitor(string id, string output, string token, CancellationToken cancellation)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#:property PublishAot=false
|
||||
// Local diagnostic only. See TranscriptFileShareProbe.md for the contract and limits.
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
const string original = "---\ntitle: transcript probe\n---\n\n# Meeting Transcript\n";
|
||||
const string written = original + "[00:00:04] Guest-1: Azure returned ***** here.\n";
|
||||
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-file-share-probe", Guid.NewGuid().ToString("N"));
|
||||
if (Directory.Exists(root))
|
||||
throw new IOException("Probe directory already exists; refusing to reuse it.");
|
||||
Directory.CreateDirectory(root);
|
||||
var cases = new List<WriteObservation>();
|
||||
var cleanupCompleted = false;
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(root, "original-reader.md");
|
||||
await File.WriteAllTextAsync(path, original);
|
||||
using (var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
||||
{
|
||||
if (reader.ReadToEnd() != original)
|
||||
throw new InvalidDataException("Original reader did not read the initial fixture.");
|
||||
cases.Add(await ObserveWriteAsync("held-original-reader", path, written));
|
||||
}
|
||||
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
|
||||
cases.Add(await ObserveWriteAsync("original-reader-released", path, written));
|
||||
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
|
||||
|
||||
path = Path.Combine(root, "compatible-reader.md");
|
||||
await File.WriteAllTextAsync(path, original);
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
||||
{
|
||||
if (reader.ReadToEnd() != original)
|
||||
throw new InvalidDataException("Compatible reader did not read the initial fixture.");
|
||||
cases.Add(await ObserveWriteAsync("held-compatible-reader", path, written));
|
||||
}
|
||||
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
cleanupCompleted = !Directory.Exists(root);
|
||||
}
|
||||
|
||||
var windowsContractMatched = OperatingSystem.IsWindows()
|
||||
? !cases[0].Completed && cases[0].Error is { Type: "System.IO.IOException", NativeCode: 32 }
|
||||
&& cases[0].ContentAfterReaderClosed == original
|
||||
: (bool?)null;
|
||||
var compatibleAndReleasedWritesCompleted = cases.Skip(1).All(result =>
|
||||
result.Completed && result.Error is null && result.ContentAfterReaderClosed == written);
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
Schema = "meeting-assistant-file-share-probe/v1",
|
||||
Runtime = RuntimeInformation.FrameworkDescription,
|
||||
OS = RuntimeInformation.OSDescription,
|
||||
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
||||
ProbeDirectory = root,
|
||||
CleanupCompleted = cleanupCompleted,
|
||||
WindowsContractMatched = windowsContractMatched,
|
||||
CompatibleAndReleasedWritesCompleted = compatibleAndReleasedWritesCompleted,
|
||||
Cases = cases,
|
||||
EvidenceLimit = "Held-reader access-mode probe; it does not reproduce the timing or establish the cause of Run4174."
|
||||
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||
return cleanupCompleted && compatibleAndReleasedWritesCompleted && windowsContractMatched != false ? 0 : 1;
|
||||
|
||||
static async Task<WriteObservation> ObserveWriteAsync(string name, string path, string content)
|
||||
{
|
||||
var elapsed = Stopwatch.StartNew();
|
||||
Task? write = null;
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try
|
||||
{
|
||||
write = File.WriteAllTextAsync(path, content, deadline.Token);
|
||||
await write;
|
||||
return new(name, true, write.Status.ToString(), elapsed.ElapsedMilliseconds, null, null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return new(name, false, write?.Status.ToString() ?? "not-returned", elapsed.ElapsedMilliseconds,
|
||||
new(exception.GetType().FullName!, $"0x{exception.HResult:X8}", exception.HResult & 0xffff, exception.Message), null);
|
||||
}
|
||||
}
|
||||
|
||||
sealed record WriteObservation(string Name, bool Completed, string TaskStatus, long ElapsedMilliseconds,
|
||||
WriteError? Error, string? ContentAfterReaderClosed);
|
||||
sealed record WriteError(string Type, string HResult, int NativeCode, string Message);
|
||||
@@ -0,0 +1,25 @@
|
||||
# Transcript file sharing diagnostic
|
||||
|
||||
Purpose: distinguish a writer error from a completed write when a reader with the original test's access mode remains open. This temporary diagnostic does not change the app, its tests, or their 577-case count.
|
||||
|
||||
Entry point: `tools/ci/TranscriptFileShareProbe.cs`, a .NET 10 file-based app with BCL-only dependencies. From this clone:
|
||||
|
||||
```sh
|
||||
/Users/dh/.dotnet/dotnet run --file tools/ci/TranscriptFileShareProbe.cs
|
||||
```
|
||||
|
||||
On another machine use its .NET 10 SDK executable. The file-based app requires an SDK supporting file-based apps; the prepared local run uses SDK 10.0.401. Native AOT is disabled for this diagnostic so its JSON report can use the normal reflection serializer. It prints JSON with runtime/OS, write completion, exception type/HResult/native error code, content after reader disposal, and cleanup status. It creates a unique directory beneath the system temporary directory, writes two small fixture files, and deletes only that directory in `finally`. It starts no Meeting Assistant app, service, network client, container, or VM. The SDK can create its normal file-based build cache. For a fresh CLI profile set `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` and `DOTNET_CLI_TELEMETRY_OPTOUT=1` to disable unrelated certificate/telemetry initialization.
|
||||
|
||||
The optional instrumentation in the existing recording-coordinator test observes public provider and transcript-store boundaries: audio consumed, fake segment yielded, append/rewrite entered, completed, or failed. Timeout output uses `[DEBUG-transcript-write-4174]` and includes elapsed milliseconds, exception type, HResult, and message. The reader, 15-second wait and final redaction assertions are unchanged. `StopAsync` always runs in `finally`; a stop failure does not mask the original timeout. Timestamps distinguish events before and after the failed wait. Instrumentation can affect race timing; a passing run alone does not explain the original failure. This temporary instrumentation should be removed after the actual Wine incident is explained.
|
||||
|
||||
The original-reader case uses the same path-taking `StreamReader` constructor as `File.ReadAllText`. It deliberately holds the reader after reading the fixture so that the overlap is deterministic. The original test normally disposes that reader immediately after `ReadToEnd`; therefore this probe checks compatible access modes, not the historical race's timing. The second write happens after disposing that reader. The compatible case holds `FileAccess.Read` with `FileShare.ReadWrite | FileShare.Delete`. No reader fix is applied to the actual test.
|
||||
|
||||
The existing Wine job runs this probe after its actual Windows SDK build and before the unchanged full test cohort. It writes `artifacts/tests/transcript-file-sharing.json` and prints that report into the CI log. Invocation there uses the already installed Windows SDK through the existing `WINE_BIN`; no runner or infrastructure capability is added. The measured Wine result is pending until this exact workflow executes.
|
||||
|
||||
## Primary-source contract
|
||||
|
||||
In [.NET runtime v10.0.12 File.cs](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L572), `ReadAllText` constructs a path-taking `StreamReader`; [`StreamReader.cs`](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L203) opens read access sharing only further readers. `WriteAllTextAsync` delegates to a create-mode write; [its writer](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L1416) opens write access with reader sharing.
|
||||
|
||||
[Windows CreateFileW documentation](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters) requires existing access and sharing modes to remain compatible until handle closure. A held reader that does not permit writes therefore prevents that writer from opening; the Windows contract expects an `IOException` with sharing-violation native code 32. Allowing read/write sharing removes that incompatibility. Delete sharing is included for the comparison but this probe does not rename or delete an open file.
|
||||
|
||||
On Windows the CLI asserts that the original held-reader write fails with code 32 and leaves the original bytes, and that both subsequent writes complete with the expected content. On other platforms it reports the original-reader observation without asserting Windows behavior (`WindowsContractMatched: null`), and still checks completed compatible/released writes and cleanup. The Unix/macOS implementation can differ. A local macOS success is not evidence of Wine behavior or the cause of Run4174's timeout.
|
||||
@@ -14,9 +14,6 @@ TIMER_FIFO="/tmp/native-diagnostic-$PROOF_TOKEN-$$.fifo"
|
||||
PENDING_OUTPUTS=()
|
||||
ACTIVE_COMMAND=""
|
||||
ACTIVE_TIMER=""
|
||||
# BEGIN disk IPC diagnostic
|
||||
ACTIVE_OBSERVER=""
|
||||
# END disk IPC diagnostic
|
||||
os_version=""
|
||||
architecture=""
|
||||
uid=-1
|
||||
@@ -99,92 +96,8 @@ read_scalar() {
|
||||
SCALAR="$value"
|
||||
}
|
||||
|
||||
# BEGIN successful sw_vers version parser
|
||||
read_product_version() {
|
||||
local value status line version="" fields=0
|
||||
# Read the entire successful native output. EOF is mandatory; a NUL delimiter
|
||||
# or reaching the 1025-byte sentinel must never hide a suffix.
|
||||
LC_ALL=C IFS= read -r -n 1025 -d '' value < "$LAST_OUTPUT"; status=$?
|
||||
(( status == 1 && ${#value} <= 1024 )) || return 1
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^[[:blank:]]*ProductVersion: ]]; then
|
||||
fields=$((fields + 1))
|
||||
(( fields == 1 )) || return 1
|
||||
[[ "$line" =~ ^[[:blank:]]*ProductVersion:[[:blank:]]*([0-9]+\.[0-9]+(\.[0-9]+)?)[[:blank:]]*$ ]] || return 1
|
||||
version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
done <<< "$value"
|
||||
(( fields == 1 )) || return 1
|
||||
SCALAR="$version"
|
||||
}
|
||||
|
||||
# END successful sw_vers version parser
|
||||
# BEGIN disk IPC diagnostic
|
||||
# Optional observations have their own child/timer ownership. No service is
|
||||
# loaded, restarted or changed, and samples target only this probe's diskutil.
|
||||
observe_disk_query() {
|
||||
local disk_process="$1" output="$2" sample_output="$3" observation_child="" observation_timer=""
|
||||
cancel_observation() {
|
||||
trap '' TERM INT
|
||||
if [ -n "$observation_child" ]; then
|
||||
kill -TERM "$observation_child" 2>/dev/null || :
|
||||
IFS= read -r -t 2 -u 9 unused || :
|
||||
kill -KILL "$observation_child" 2>/dev/null || :
|
||||
wait "$observation_child" 2>/dev/null || :
|
||||
fi
|
||||
[ -z "$observation_timer" ] || { kill -TERM "$observation_timer" 2>/dev/null || :; wait "$observation_timer" 2>/dev/null || :; }
|
||||
printf '[disk-observation] stopped after the owned disk query\n' >> "$output"
|
||||
exit 143
|
||||
}
|
||||
observe_command() {
|
||||
local name="$1" status started=$SECONDS
|
||||
shift
|
||||
printf '\n[disk-observation-command] %s:' "$name" >> "$output"
|
||||
printf ' %s' "$@" >> "$output"
|
||||
printf '\n' >> "$output"
|
||||
"$@" >> "$output" 2>&1 &
|
||||
observation_child=$!
|
||||
(
|
||||
trap 'exit 0' TERM INT
|
||||
IFS= read -r -t 60 -u 9 unused || :
|
||||
printf '[disk-observation-timeout] %s child=%s limit=60s\n' "$name" "$observation_child" >> "$output"
|
||||
kill -TERM "$observation_child" 2>/dev/null || :
|
||||
IFS= read -r -t 2 -u 9 unused || :
|
||||
kill -KILL "$observation_child" 2>/dev/null || :
|
||||
) &
|
||||
observation_timer=$!
|
||||
wait "$observation_child"; status=$?
|
||||
kill -TERM "$observation_timer" 2>/dev/null || :
|
||||
wait "$observation_timer" 2>/dev/null || :
|
||||
printf '[disk-observation-exit] %s status=%s elapsed=%ss\n' "$name" "$status" "$((SECONDS - started))" >> "$output"
|
||||
observation_child=""; observation_timer=""
|
||||
}
|
||||
trap cancel_observation TERM INT
|
||||
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
|
||||
if [ -x /usr/bin/sample ]; then
|
||||
if kill -0 "$disk_process" 2>/dev/null; then
|
||||
observe_command diskutil-sample /usr/bin/sample "$disk_process" 3 100 -file "$sample_output"
|
||||
else
|
||||
printf '[disk-observation-unavailable] diskutil already exited before sample\n' >> "$output"
|
||||
fi
|
||||
else
|
||||
printf '[disk-observation-unavailable] /usr/bin/sample is unavailable\n' >> "$output"
|
||||
fi
|
||||
}
|
||||
|
||||
stop_disk_observation() {
|
||||
[ -n "$ACTIVE_OBSERVER" ] || return 0
|
||||
kill -TERM "$ACTIVE_OBSERVER" 2>/dev/null || :
|
||||
wait "$ACTIVE_OBSERVER" 2>/dev/null || :
|
||||
ACTIVE_OBSERVER=""
|
||||
}
|
||||
|
||||
# END disk IPC diagnostic
|
||||
cancel_probe() {
|
||||
trap '' TERM INT
|
||||
# BEGIN disk IPC diagnostic
|
||||
stop_disk_observation
|
||||
# END disk IPC diagnostic
|
||||
if [ -n "$ACTIVE_COMMAND" ]; then
|
||||
kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || :
|
||||
IFS= read -r -t 2 -u 9 unused || :
|
||||
@@ -203,9 +116,6 @@ run_command() {
|
||||
# Run 4161: even native uname/ps startup took 34-42s under TCG.
|
||||
# Isolate only the failed UID gate; every other watchdog remains unchanged.
|
||||
[[ "$name" != uid ]] || command_limit=180
|
||||
# BEGIN disk IPC diagnostic
|
||||
if [[ "$name" == disks && "${attempt:-0}" == 1 ]]; then command_limit=120; fi
|
||||
# END disk IPC diagnostic
|
||||
LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
|
||||
printf '\n[proof-command] %s:' "$name" >&3
|
||||
printf ' %s' "$@" >&3
|
||||
@@ -226,18 +136,6 @@ run_command() {
|
||||
) &
|
||||
timer=$!
|
||||
ACTIVE_TIMER="$timer"
|
||||
# BEGIN disk IPC diagnostic
|
||||
if [[ "$name" == disks && "$attempt" == 1 ]]; then
|
||||
local observation_output="/tmp/native-diagnostic-disk-observation.out"
|
||||
local sample_output="/tmp/native-diagnostic-disk-sample.out"
|
||||
: > "$observation_output"
|
||||
: > "$sample_output"
|
||||
observe_disk_query "$process" "$observation_output" "$sample_output" &
|
||||
ACTIVE_OBSERVER=$!
|
||||
printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3
|
||||
PENDING_OUTPUTS+=("$observation_output" "$sample_output")
|
||||
fi
|
||||
# END disk IPC diagnostic
|
||||
wait "$process"
|
||||
exit_code=$?
|
||||
waited=$SECONDS
|
||||
@@ -245,9 +143,6 @@ run_command() {
|
||||
printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3
|
||||
kill -TERM "$timer" 2>/dev/null || :
|
||||
wait "$timer" 2>/dev/null || :
|
||||
# BEGIN disk IPC diagnostic
|
||||
stop_disk_observation
|
||||
# END disk IPC diagnostic
|
||||
ACTIVE_COMMAND=""; ACTIVE_TIMER=""
|
||||
printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3
|
||||
printf '[proof-exit] %s\n' "$exit_code" >&3
|
||||
@@ -300,25 +195,18 @@ if (( platform_exit != 0 )); then
|
||||
platform_exit="$LAST_EXIT"
|
||||
fi
|
||||
(( platform_exit == 0 )) || fail_probe sw_vers_failed
|
||||
# BEGIN successful sw_vers version extraction
|
||||
read_product_version || fail_probe product_version_invalid
|
||||
# END successful sw_vers version extraction
|
||||
run_command version /usr/bin/sw_vers -productVersion
|
||||
(( LAST_EXIT == 0 )) || fail_probe product_version_failed
|
||||
read_scalar || fail_probe product_version_invalid
|
||||
os_version="$SCALAR"
|
||||
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
|
||||
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version
|
||||
flush_outputs || finish false diagnostic_log_budget_exceeded
|
||||
|
||||
# BEGIN disk IPC diagnostic
|
||||
run_command arbitration_before /bin/launchctl print system/com.apple.diskarbitrationd
|
||||
run_command management_before /bin/launchctl print system/com.apple.diskmanagementd
|
||||
run_command media_before /usr/sbin/ioreg -r -c IOMedia -l -w 0
|
||||
flush_outputs || finish false diagnostic_log_budget_exceeded
|
||||
|
||||
# END disk IPC diagnostic
|
||||
# Bound readiness independently of the host's 40-minute overall deadline.
|
||||
readiness_start=$SECONDS
|
||||
attempt=0
|
||||
while (( attempt < 1 && SECONDS - readiness_start < 600 )); do
|
||||
while (( SECONDS - readiness_start < 600 )); do
|
||||
attempt=$((attempt + 1))
|
||||
printf '\n[readiness-attempt] %s\n' "$attempt" >&3
|
||||
run_command disks /usr/sbin/diskutil list physical
|
||||
|
||||
Reference in new issue
Block a user