1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 03:10:06 +01:00

♻️ Reworked how getting a CaskFile via URL works

This commit is contained in:
2025-11-18 15:39:43 +01:00
parent ec9102618c
commit d6fa3fc364
11 changed files with 216 additions and 44 deletions

View File

@@ -84,6 +84,11 @@
039E1D7A2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
039E1D7C2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
03ACC6452ECCAB190070D4CD /* RealWebApiTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */; };
03ACC6472ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
03ACC6492ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
03ACC64A2ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
03B675E92EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03B675EA2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03B675EB2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
@@ -1038,6 +1043,8 @@
039C29172E8AA311007F5FAB /* TestableWebApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApi.swift; sourceTree = "<group>"; };
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApiTest.swift; sourceTree = "<group>"; };
039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = "<group>"; };
03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealWebApiTest.swift; sourceTree = "<group>"; };
03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaskFile+API.swift"; sourceTree = "<group>"; };
03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = "<group>"; };
03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = "<group>"; };
03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = "<group>"; };
@@ -1455,11 +1462,19 @@
039C291E2E8AA39B007F5FAB /* Api */ = {
isa = PBXGroup;
children = (
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */,
);
path = Api;
sourceTree = "<group>";
};
03ACC6432ECCAAF70070D4CD /* WebApi */ = {
isa = PBXGroup;
children = (
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */,
03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */,
);
path = WebApi;
sourceTree = "<group>";
};
03BFF1D12E3CF4F2004C56A9 /* Provision */ = {
isa = PBXGroup;
children = (
@@ -1485,6 +1500,7 @@
C4E2E84628FC1D8C003B070C /* TestableConfigurationTest.swift */,
C413E43328DA3E8F00AE33C7 /* Shell */,
C471E6DA28F9AFCB0021E251 /* Filesystem */,
03ACC6432ECCAAF70070D4CD /* WebApi */,
);
path = Testables;
sourceTree = "<group>";
@@ -2145,6 +2161,7 @@
C4B79EB529CA387F00A483EE /* BrewPhpFormulaeHandler.swift */,
C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */,
C40934A1298EEB2C00D25014 /* CaskFile.swift */,
03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */,
C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */,
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */,
);
@@ -2914,6 +2931,7 @@
C45D654C29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */,
C4D3660B29113F20006BD146 /* System.swift in Sources */,
C4D36601291132B7006BD146 /* ValetScanners.swift in Sources */,
03ACC6472ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */,
C4B79EB629CA387F00A483EE /* BrewPhpFormulaeHandler.swift in Sources */,
@@ -3143,6 +3161,7 @@
C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */,
C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */,
03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
C471E82A28F9BB330021E251 /* ValetListable.swift in Sources */,
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */,
C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */,
@@ -3251,6 +3270,7 @@
C471E8B428F9BB8F0021E251 /* MainMenu+Switcher.swift in Sources */,
C471E8B528F9BB8F0021E251 /* MainMenu+FixMyValet.swift in Sources */,
C471E8B628F9BB8F0021E251 /* MainMenu+Actions.swift in Sources */,
03ACC64A2ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
C471E8B728F9BB8F0021E251 /* StatusMenu.swift in Sources */,
032DAC302E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */,
C471E8B828F9BB8F0021E251 /* StatusMenu+Items.swift in Sources */,
@@ -3547,6 +3567,7 @@
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C4AD38B328ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */,
03ACC6492ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
C40C5C9D2846A40600E28255 /* Preset.swift in Sources */,
C4CE7F9729683B43000102CF /* PhpVersionNumberCollection.swift in Sources */,
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
@@ -3557,6 +3578,7 @@
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */,
C4611E612AEAD3110010BE24 /* ByteLimitView.swift in Sources */,
C40175B92903108900763A68 /* ValetInteractor.swift in Sources */,
03ACC6452ECCAB190070D4CD /* RealWebApiTest.swift in Sources */,
039E1D792E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */,
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,

View File

@@ -11,15 +11,81 @@ import Foundation
class RealWebApi: WebApiProtocol {
var container: Container
var defaultHeaders: HttpHeaders {
return [
"User-Agent": "phpmon-nur/2.0",
"X-phpmon-version": "\(App.shortVersion) (\(App.bundleVersion))",
"X-phpmon-os-version": "\(App.macVersion)",
"X-phpmon-bundle-id": "\(App.identifier)"
]
}
init(container: Container) {
self.container = container
}
func get(_ url: URL, withHeaders: HttpHeaders, withTimeout: TimeInterval) async throws -> WebApiResponse {
return WebApiResponse(statusCode: 200, headers: [:], data: Data())
private func request(
url: URL,
method: String,
data: Data?,
headers: HttpHeaders,
timeout: TimeInterval
) async throws -> WebApiResponse {
var request = URLRequest(url: url)
request.httpMethod = method
request.timeoutInterval = timeout
for header in headers {
request.setValue(header.value, forHTTPHeaderField: header.key)
}
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw WebApiError.networkError
}
return WebApiResponse(
statusCode: response.statusCode,
headers: response.allHeaderFields as! HttpHeaders,
data: data
)
} catch {
if let urlError = error as? URLError {
if urlError.code == .timedOut {
throw WebApiError.timedOut
}
}
throw WebApiError.networkError
}
}
func post(_ url: URL, withHeaders: HttpHeaders, withData: String, withTimeout: TimeInterval) async throws -> WebApiResponse {
return WebApiResponse(statusCode: 200, headers: [:], data: Data())
func get(
_ url: URL,
withHeaders headers: HttpHeaders = [:],
withTimeout timeout: TimeInterval = URLSession.shared.configuration.timeoutIntervalForRequest
) async throws -> WebApiResponse {
try await self.request(
url: url,
method: "GET",
data: nil,
headers: headers,
timeout: timeout
)
}
func post(
_ url: URL,
withHeaders headers: HttpHeaders,
withData data: String,
withTimeout timeout: TimeInterval
) async throws -> WebApiResponse {
try await self.request(
url: url,
method: "POST",
data: data.data(using: .utf8),
headers: headers,
timeout: timeout
)
}
}

View File

@@ -9,14 +9,33 @@
import Foundation
class TestableWebApi: WebApiProtocol {
// MARK: - Internal Fake Responses
private var fakeGetResponses: [URL: FakeWebApiResponse] = [:]
private var fakePostResponses: [URL: FakeWebApiResponse] = [:]
// MARK: - Slow Mode
private var slow: Bool = false
public func setSlowMode(_ slow: Bool) {
self.slow = slow
}
// MARK: - Default Headers
var defaultHeaders: HttpHeaders {
return [
"User-Agent": "phpmon-nur/2.0",
"X-phpmon-version": "\(App.shortVersion) (\(App.bundleVersion))",
"X-phpmon-os-version": "\(App.macVersion)",
"X-phpmon-bundle-id": "\(App.identifier)"
]
}
// MARK: - Constructor
init(
getResponses: [URL: FakeWebApiResponse],
postResponses: [URL: FakeWebApiResponse]
@@ -25,6 +44,8 @@ class TestableWebApi: WebApiProtocol {
self.fakePostResponses = postResponses
}
// MARK: - Public API
public func hasGetResponse(for url: URL) -> Bool {
return fakeGetResponses.keys.contains(url)
}

View File

@@ -14,6 +14,7 @@ enum WebApiError: Error {
case invalidURL
case networkError
case timedOut
case other
}
struct WebApiResponse {
@@ -37,6 +38,8 @@ struct WebApiResponse {
}
protocol WebApiProtocol {
var defaultHeaders: HttpHeaders { get }
func get(
_ url: URL,
withHeaders headers: HttpHeaders,

View File

@@ -28,8 +28,7 @@ class AppUpdater {
let caskUrl = Constants.Urls.UpdateCheckEndpoint
guard let caskFile = await CaskFile.from(App.shared.container, url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
guard let caskFile = try? await CaskFile.fromUrl(App.shared.container, caskUrl) else {
presentCouldNotRetrieveUpdateIfInteractive()
return .networkError
}

View File

@@ -101,15 +101,11 @@ class CrashReporter {
request.setValue("text/crash", forHTTPHeaderField: "Content-Type")
request.setValue("phpmon-crashrep/1.0", forHTTPHeaderField: "User-Agent")
request.httpBody = text.data(using: .utf8)
request.timeoutInterval = timeout
// Send the request synchronously, we want the report to be sent before anything else
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = timeout
let session = URLSession(configuration: config)
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request) { _, response, error in
let task = URLSession.shared.dataTask(with: request) { _, response, error in
defer { semaphore.signal() }
if let error = error {

View File

@@ -0,0 +1,52 @@
//
// CaskFile+API.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
enum CaskFileError: Error {
case requestFailed
case invalidData
case invalidFile
}
extension CaskFile {
public static func fromUrl(
_ container: Container,
_ url: URL
) async throws -> CaskFile? {
// First, determine if we're loading a local URL or not
if url.scheme == "file" {
if let string = try? container.filesystem.getStringFromFile(url.relativePath) {
return CaskFile.from(string)
} else {
throw CaskFileError.invalidFile
}
}
// If it's a URL via the network we need to know if to use the complex API or not
if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
// For testing simplicity, we will use curl, since we need no complex rules or headers
return CaskFile.from(await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out)
} else {
// However, for the real deal, we will use the Web API
guard let response = try? await container.webApi.get(
url,
withHeaders: container.webApi.defaultHeaders,
withTimeout: .seconds(10)
) else {
throw CaskFileError.requestFailed
}
guard let text = response.plainText else {
throw CaskFileError.invalidData
}
return CaskFile.from(text)
}
}
}

View File

@@ -24,35 +24,7 @@ struct CaskFile {
return self.properties["version"]!
}
private static func loadFromApi(_ container: Container, _ url: URL) async -> String {
if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
return await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
} else {
return await container.shell.pipe("""
curl -s --max-time 10 \
-H "User-Agent: phpmon-curl/1.0" \
-H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \
-H "X-phpmon-os-version: \(App.macVersion)" \
-H "X-phpmon-bundle-id: \(App.identifier)" \
'\(url.absoluteString)'
""").out
}
}
public static func from(_ container: Container, url: URL) async -> CaskFile? {
var string: String?
if url.scheme == "file" {
string = try? String(contentsOf: url)
} else {
string = await CaskFile.loadFromApi(container, url)
}
guard let string else {
Log.err("The content of the URL for the CaskFile could not be retrieved")
return nil
}
public static func from(_ string: String) -> CaskFile? {
let lines = string.split(separator: "\n")
.map { line in
return line.trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -27,7 +27,7 @@ struct CaskFileParserTest {
}
@Test func can_extract_fields_from_cask_file() async throws {
guard let caskFile = await CaskFile.from(container, url: CaskFileParserTest.exampleFilePath) else {
guard let caskFile = try? await CaskFile.fromUrl(container, CaskFileParserTest.exampleFilePath) else {
Issue.record("The CaskFile could not be parsed, check the log for more info")
return
}
@@ -53,7 +53,7 @@ struct CaskFileParserTest {
@Test func can_extract_fields_from_remote_cask_file() async throws {
let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")!
guard let caskFile = await CaskFile.from(container, url: url) else {
guard let caskFile = try? await CaskFile.fromUrl(container, url) else {
Issue.record("The remote CaskFile could not be parsed, check the log for more info")
return
}

View File

@@ -0,0 +1,41 @@
//
// RealWebApiTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 18/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Testing
import Foundation
struct RealWebApiTest {
private var container: Container
init() throws {
self.container = Container()
container.bind()
}
var WebApi: RealWebApi {
return container.webApi as! RealWebApi
}
@Test func requestSucceeds() async {
let response = try! await WebApi.get(
url("https://api.phpmon.test/up")
)
#expect(response.statusCode == 200)
#expect(response.plainText!.contains("Response rendered in"))
}
@Test func requestTimesOut() async {
await #expect(throws: WebApiError.timedOut) {
try await WebApi.get(
url("https://api.phpmon.test/up"),
withTimeout: .seconds(0.01)
)
}
}
}