Nucleic: Gitea Runner macOS VM Support
This commit is contained in:
@@ -88,8 +88,10 @@ public enum Doctor {
|
||||
/// than at the first poll.
|
||||
/// 8. **Registration token resolvable** from file, inline value, or (if
|
||||
/// enabled) the API.
|
||||
/// 9. **Runner download URL is live**, via a `HEAD` expecting 200. Catches a
|
||||
/// version bump that no longer has a darwin-arm64 asset.
|
||||
/// 9. **Runner download URL is live**, via a one-byte ranged `GET` — the
|
||||
/// same verb the real download uses, because the presigned redirect
|
||||
/// target is signed per method. Catches a version bump that no longer
|
||||
/// has a darwin-arm64 asset.
|
||||
/// 10. **Local Network privacy note** (informational). On macOS 15+ the
|
||||
/// first attempt to reach a guest over the NAT link can be blocked by
|
||||
/// the Local Network permission prompt, which a background agent cannot
|
||||
@@ -455,16 +457,26 @@ public enum Doctor {
|
||||
)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD"
|
||||
request.timeoutInterval = 15
|
||||
|
||||
// A ranged GET, not a HEAD. gitea.com answers an asset request with a
|
||||
// 303 to a presigned object-storage URL, and the signature covers the
|
||||
// *method of the request that minted it*: ask with HEAD and you get a
|
||||
// HEAD-signed URL. URLSession then follows the 303 and — per RFC 7231
|
||||
// §6.4.4 — rewrites the method to GET, so the signed URL is replayed
|
||||
// with the one verb it was not signed for and the store answers 403
|
||||
// SignatureDoesNotMatch. Probing with the same verb the real download
|
||||
// uses is the only way to make the answer mean anything. `bytes=0-0`
|
||||
// keeps it to one byte instead of the whole 20-plus MB asset.
|
||||
do {
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
let status = try await probeStatus(url: url, method: "GET", range: "bytes=0-0")
|
||||
if status <= 399 {
|
||||
return DoctorCheck(name: name, result: .pass, detail: "\(url.absoluteString) → \(status)")
|
||||
}
|
||||
// A host that rejects ranges outright still deserves a second look
|
||||
// before we call the asset missing.
|
||||
let fallback = try await probeStatus(url: url, method: "HEAD", range: nil)
|
||||
if fallback <= 399 {
|
||||
return DoctorCheck(name: name, result: .pass, detail: "\(url.absoluteString) → \(fallback)")
|
||||
}
|
||||
return DoctorCheck(
|
||||
name: name,
|
||||
result: .warn,
|
||||
@@ -483,6 +495,19 @@ public enum Doctor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Issues one probe request and reports its status code, or 0 if the
|
||||
/// response was not HTTP.
|
||||
private static func probeStatus(url: URL, method: String, range: String?) async throws -> Int {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.timeoutInterval = 15
|
||||
if let range {
|
||||
request.setValue(range, forHTTPHeaderField: "Range")
|
||||
}
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
return (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||
}
|
||||
|
||||
/// Warns about token files readable by other users on this Mac.
|
||||
public static func checkTokenFilePermissions(config: RunnerConfig) -> [DoctorCheck] {
|
||||
let insecure = config.insecureTokenFilePaths
|
||||
|
||||
Reference in New Issue
Block a user