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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.8.0</string>
<string>0.8.1</string>
<key>CFBundleVersion</key>
<string>8</string>
<string>9</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down
16 changes: 16 additions & 0 deletions Scripts/smoke_babeldoc_runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"

APP_VERSION="$(
/usr/libexec/PlistBuddy \
-c 'Print :CFBundleShortVersionString' \
Resources/Info.plist
)"

GLOSS_RUN_BABELDOC_RUNTIME_SMOKE=1 \
GLOSS_RUNTIME_SMOKE_APP_VERSION="$APP_VERSION" \
swift test \
--filter BabelDOCRuntimeDistributionTests/publicRuntimeReleaseInstallsEndToEnd
45 changes: 36 additions & 9 deletions Sources/GlossCore/BabelDOCRuntimeDistribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,33 @@ public struct BabelDOCRuntimeTransport: Sendable {
public static let live = ephemeral()

public static func ephemeral(
policy: BabelDOCRuntimeNetworkPolicy = BabelDOCRuntimeNetworkPolicy()
policy: BabelDOCRuntimeNetworkPolicy
) -> Self {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = policy.requestTimeout
configuration.timeoutIntervalForResource = policy.resourceTimeout
configuration.waitsForConnectivity = policy.waitsForConnectivity
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
let session = URLSession(configuration: configuration)
ephemeral(
metadataPolicy: policy,
downloadPolicy: policy
)
}

public static func ephemeral(
metadataPolicy: BabelDOCRuntimeNetworkPolicy = .metadata,
downloadPolicy: BabelDOCRuntimeNetworkPolicy = .runtimeArchive
) -> Self {
let metadataSession = URLSession(
configuration: sessionConfiguration(for: metadataPolicy)
)
let downloadSession = URLSession(
configuration: sessionConfiguration(for: downloadPolicy)
)

return Self(
fetchData: { url in
let (data, response) = try await session.data(from: url)
let (data, response) = try await metadataSession.data(from: url)
try validateHTTPResponse(response, for: url)
return data
},
download: { url, destination in
let (temporaryURL, response) = try await session.download(from: url)
let (temporaryURL, response) = try await downloadSession.download(from: url)
try validateHTTPResponse(response, for: url)

let fileManager = FileManager.default
Expand All @@ -216,6 +226,17 @@ public struct BabelDOCRuntimeTransport: Sendable {
)
}

static func sessionConfiguration(
for policy: BabelDOCRuntimeNetworkPolicy
) -> URLSessionConfiguration {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = policy.requestTimeout
configuration.timeoutIntervalForResource = policy.resourceTimeout
configuration.waitsForConnectivity = policy.waitsForConnectivity
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
return configuration
}

private static func validateHTTPResponse(_ response: URLResponse, for url: URL) throws {
guard let response = response as? HTTPURLResponse else {
return
Expand All @@ -230,6 +251,12 @@ public struct BabelDOCRuntimeTransport: Sendable {
}

public struct BabelDOCRuntimeNetworkPolicy: Equatable, Sendable {
public static let metadata = Self()
public static let runtimeArchive = Self(
requestTimeout: 60,
resourceTimeout: 60 * 60
)

public let requestTimeout: TimeInterval
public let resourceTimeout: TimeInterval
public let waitsForConnectivity: Bool
Expand Down
70 changes: 64 additions & 6 deletions Tests/GlossCoreTests/BabelDOCRuntimeDistributionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,71 @@ struct BabelDOCRuntimeDistributionTests {
)
}

@Test("live transport uses bounded offline timeouts")
func liveNetworkPolicyIsBounded() {
let policy = BabelDOCRuntimeNetworkPolicy()
@Test("live transport keeps metadata fast and allows large archive downloads")
func liveNetworkPoliciesMatchPayloadSize() {
let metadataPolicy = BabelDOCRuntimeNetworkPolicy.metadata
let archivePolicy = BabelDOCRuntimeNetworkPolicy.runtimeArchive
let metadataConfiguration = BabelDOCRuntimeTransport.sessionConfiguration(
for: metadataPolicy
)
let archiveConfiguration = BabelDOCRuntimeTransport.sessionConfiguration(
for: archivePolicy
)

#expect(metadataConfiguration.timeoutIntervalForRequest == 12)
#expect(metadataConfiguration.timeoutIntervalForResource == 60)
#expect(!metadataConfiguration.waitsForConnectivity)
#expect(archiveConfiguration.timeoutIntervalForRequest == 60)
#expect(archiveConfiguration.timeoutIntervalForResource == 60 * 60)
#expect(!archiveConfiguration.waitsForConnectivity)
}

@Test(
"public runtime release installs and restores end to end",
.enabled(
if: ProcessInfo.processInfo.environment[
"GLOSS_RUN_BABELDOC_RUNTIME_SMOKE"
] == "1",
"Set GLOSS_RUN_BABELDOC_RUNTIME_SMOKE=1 to download the public runtime."
)
)
func publicRuntimeReleaseInstallsEndToEnd() async throws {
let currentGlossVersion = try #require(
ProcessInfo.processInfo.environment[
"GLOSS_RUNTIME_SMOKE_APP_VERSION"
]
)

#expect(policy.requestTimeout == 12)
#expect(policy.resourceTimeout == 60)
#expect(!policy.waitsForConnectivity)
let root = try temporaryDirectory()
defer {
try? FileManager.default.removeItem(at: root)
}
let manager = try BabelDOCRuntimeManager(
rootDirectory: root,
currentGlossVersion: currentGlossVersion
)

let installed = try await manager.update { progress in
print(
"BabelDOC runtime smoke: \(progress.operation.rawValue)"
+ (progress.version.map { " \($0)" } ?? "")
)
}
let installedVersion = try #require(installed.currentVersion)
let executableURL = try #require(installed.currentExecutableURL)
#expect(installed.operation == .ready)
#expect(installed.lastError == nil)
#expect(FileManager.default.isExecutableFile(atPath: executableURL.path))

let restoredManager = try BabelDOCRuntimeManager(
rootDirectory: root,
currentGlossVersion: currentGlossVersion
)
let restored = await restoredManager.snapshot()
#expect(restored.currentVersion == installedVersion)
#expect(restored.currentExecutableURL == executableURL)
#expect(restored.operation == .idle)
#expect(restored.lastError == nil)
}

@Test("raw runtime install verifies SHA, permissions, and persisted state")
Expand Down
27 changes: 27 additions & 0 deletions docs/release-notes/v0.8.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Gloss 0.8.1

Gloss 0.8.1 fixes first-time installation of the managed BabelDOC runtime.

## PDF runtime

- Keeps signed manifest and signature requests on a short, bounded network
timeout.
- Gives the 200+ MiB runtime archive its own download session with a one-hour
total timeout and a 60-second inactivity timeout, so slow connections can
finish without hiding a stalled transfer.
- Preserves the existing single-policy transport API for callers that need the
same timeout behavior for metadata and downloads.

## Validation

- Adds an opt-in command-line smoke test that downloads the public runtime,
verifies its signed manifest, archive size and SHA-256, extracts it, checks
the executable, and restores the persisted installation state.
- The arm64 public runtime completed this path on a real slow connection in
1,766.952 seconds; the previous release stopped after 60 seconds.

## Compatibility

This release uses the same signed BabelDOC `0.6.4+gloss.4` runtime and remains
compatible with macOS 14 or newer. Existing verified runtime installations do
not need to be downloaded again.
Loading