Merge nucleic/vivid-glass-urchin-xoym into main
This commit is contained in:
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user