From e17499b816c353a2a5db8153c8f9fd6fecf3162e Mon Sep 17 00:00:00 2001 From: JonLuca DeCaro Date: Sun, 25 Oct 2026 09:21:23 -0700 Subject: [PATCH] fix(cli): synchronize HLS fixture redirect port reads --- CHANGELOG.md | 4 ++++ Sources/aetherctl/HLSFixture.swift | 4 ++-- docs/cli.md | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cadfbc99f..d4495781b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,10 @@ the public-API contract. ### Fixed +- **The HLS fixture reads its redirect port under the lifecycle lock.** A request worker could read + the bound port while `stop()` reset it under the lock. Host-only redirects now take the same lock, + and the port stays private to the server. Explicit `--redirect-port` targets are unchanged. + - **Source failures keep their cause and request ownership.** Initial live probes preserve HTTP, URL and TLS failures without changing the live-start `liveSourceUnavailable` kind (HTTP and certificate classifications stay specific). A transport failure at open (a reset or a timeout from diff --git a/Sources/aetherctl/HLSFixture.swift b/Sources/aetherctl/HLSFixture.swift index b324a879d..50e11a23b 100644 --- a/Sources/aetherctl/HLSFixture.swift +++ b/Sources/aetherctl/HLSFixture.swift @@ -424,7 +424,7 @@ final class HLSFixtureServer: @unchecked Sendable { private var shouldStop = false private let lock = NSLock() private var clientFds = Set() - private(set) var port: UInt16 = 0 + private var port: UInt16 = 0 private var startTime: Date = Date() @@ -596,7 +596,7 @@ final class HLSFixtureServer: @unchecked Sendable { // which is what a tokenized IPTV portal does (portal answers, CDN edge serves). 127.0.0.1 and // localhost are distinct origins to any HTTP client that decides header forwarding by host. if let redirectHost = config.redirectEntryToHost, path == "/entry.m3u8" { - let targetPort = config.redirectEntryToPort ?? port + let targetPort = config.redirectEntryToPort ?? lock.withLock { port } let target = "http://\(redirectHost):\(targetPort)/\(config.withMaster ? "master.m3u8" : "media.m3u8")" print("[HLSFixture] 302 /entry.m3u8 -> \(target)") send302(fd: fd, location: target) diff --git a/docs/cli.md b/docs/cli.md index 484664bb0..591a9cbe0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -887,6 +887,8 @@ aetherctl play --live --header "User-Agent: Mozilla/5.0 (QtEmbedded; TestSTB)" \ The REQ log then carries the verdict per request (`auth=ok` / `auth=MISSING -> 403`), which is what makes "who lost the header, and on which hop" a measurement. Knobs: `--require-header "Name: Value"`, `--deny-status N` (401 and 403 reach the engine as different `NSURLError` codes), `--deny-segments-only` (refuse after readyToPlay rather than at the master), `--deny-user-agent S` (refuse `AppleCoreMedia` and serve everyone else, the one origin shape that tells the AVPlayer bypass and the engine's own ingest fetcher apart), `--redirect-entry` / `--redirect-host` / `--redirect-port` (portal-to-edge 302, cross-origin by host name and port while staying on loopback), `--media-origin H:P` (master's variants point at a second origin absolutely). +Without `--redirect-port`, a redirect uses the fixture's bound port, read under the same lifecycle lock that updates it on start and stop. + Measured with it before AE#363 was written, and worth knowing before suspecting the engine: `LoadOptions.httpHeaders` survive BOTH shapes on the AVPlayer bypass on macOS, the cross-origin 302 and the absolutely referenced second origin. Every request arrived with the header and the session played through. `--codecs` / `--resolution` write `CODECS=` / `RESOLUTION=` onto both `EXT-X-STREAM-INF` lines. Without them AVFoundation reports no `videoAttributes` for the variants, so everything that reads master evidence (the #168 watchdog, the #293 probe gate) sees a master advertising no video at all and the fixture quietly stops carrying the case under test.