Merge nucleic/vivid-glass-urchin-xoym into main

This commit is contained in:
2026-08-07 15:41:01 -07:00
parent 6b0c01b74b
commit 902bea5091
11 changed files with 450 additions and 91 deletions
+57 -2
View File
@@ -47,10 +47,25 @@ public struct ServiceStatus: Sendable, Equatable {
/// because this is the failure people hit first.
public enum LaunchdService {
/// The `launchd` label, matching `CFBundleIdentifier`.
public static let label = "xyz.blakeslee.gitea-macos-runner"
public static let label = "xyz.blakeslee.gitea-macos-vm-orchestrator"
/// `~/Library/LaunchAgents/xyz.blakeslee.gitea-macos-runner.plist`.
/// Labels this service used to install under.
///
/// Renaming the label renames the plist, so an upgrade that only wrote the
/// new one would leave the old job bootstrapped and still running the old
/// binary — two daemons polling the same Gitea instance, racing to claim
/// the same queued jobs, with no hint in the logs that a second one exists.
/// ``install(executablePath:configPath:)`` and ``uninstall()`` therefore
/// evict these first. Append, never edit, when the label changes again.
public static let legacyLabels = ["xyz.blakeslee.gitea-macos-runner"]
/// `~/Library/LaunchAgents/xyz.blakeslee.gitea-macos-vm-orchestrator.plist`.
public static var agentPlistURL: URL {
agentPlistURL(for: label)
}
/// The LaunchAgent plist path for an arbitrary label.
public static func agentPlistURL(for label: String) -> URL {
URL(fileURLWithPath: RunnerConfig.expandTilde("~/Library/LaunchAgents/\(label).plist"))
}
@@ -106,6 +121,11 @@ public enum LaunchdService {
withIntermediateDirectories: true
)
// Upgrading from a build that installed under an older label: evict it
// before bootstrapping this one, or both run at once. See
// ``legacyLabels``.
removeLegacyAgents()
// A reinstall over a loaded job is the common case (upgrade, config
// change), so unload before rewriting rather than failing on "already
// bootstrapped".
@@ -140,13 +160,48 @@ public enum LaunchdService {
}
/// Unloads the job and removes the plist. Safe when not installed.
///
/// Also evicts any ``legacyLabels`` job, so `service uninstall` leaves
/// nothing of this project loaded regardless of which version installed it.
public static func uninstall() throws {
removeLegacyAgents()
_ = try? uninstallJobOnly()
if FileManager.default.fileExists(atPath: agentPlistURL.path) {
try FileManager.default.removeItem(at: agentPlistURL)
}
}
/// Boots out and deletes any LaunchAgent installed under a ``legacyLabels``
/// entry.
///
/// Best effort by design: a legacy job that was never installed, is not
/// loaded, or whose plist is already gone is not an error, and failing to
/// evict one must not block installing the current job.
///
/// - Returns: The legacy labels that were actually found and removed, for
/// callers that want to tell the operator a migration happened.
@discardableResult
public static func removeLegacyAgents() -> [String] {
var removed: [String] = []
for legacy in legacyLabels {
let plist = agentPlistURL(for: legacy)
let bootout = LaunchdShell.run(
"/bin/launchctl", ["bootout", "\(domainTarget)/\(legacy)"])
if bootout.exitCode != 0 {
_ = LaunchdShell.run("/bin/launchctl", ["unload", "-w", plist.path])
}
if FileManager.default.fileExists(atPath: plist.path) {
try? FileManager.default.removeItem(at: plist)
removed.append(legacy)
} else if bootout.exitCode == 0 {
// Loaded, but from a plist that is no longer on disk.
removed.append(legacy)
}
}
return removed
}
/// Unloads the job but leaves the plist on disk.
private static func uninstallJobOnly() throws {
let bootout = LaunchdShell.run("/bin/launchctl", ["bootout", serviceTarget])