Merge nucleic/mellow-dewy-falcon-rjhr into main
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import RunnerCore
|
||||
import RunnerHost
|
||||
|
||||
/// `gitea-macos-runner service …` — manage the `launchd` LaunchAgent.
|
||||
///
|
||||
/// - Important: This installs a **LaunchAgent** in the logged-in user's session,
|
||||
/// never a LaunchDaemon. Virtualization needs a GUI session, and macOS 15+
|
||||
/// additionally needs an unlocked `login.keychain` to start a VM — neither of
|
||||
/// which exists in the system context. The host should be set to log in
|
||||
/// automatically.
|
||||
struct ServiceCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "service",
|
||||
abstract: "Install, remove, or inspect the launchd LaunchAgent.",
|
||||
subcommands: [Install.self, Uninstall.self, Status.self]
|
||||
)
|
||||
|
||||
/// `service install` — write the plist and load the job.
|
||||
struct Install: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "install",
|
||||
abstract: "Write ~/Library/LaunchAgents/xyz.blakeslee.gitea-macos-runner.plist and load it.",
|
||||
discussion: """
|
||||
Points the agent at the installed, signed .app bundle — not at a bare \
|
||||
binary. The com.apple.security.virtualization entitlement only survives \
|
||||
on the signed bundle, so a daemon started from .build/ cannot start VMs.
|
||||
"""
|
||||
)
|
||||
|
||||
@OptionGroup var options: GlobalOptions
|
||||
|
||||
/// Path to the installed executable inside the signed `.app`.
|
||||
@Option(name: .long, help: "Path to the installed executable (default: ~/Applications/GiteaMacosRunner.app/Contents/MacOS/gitea-macos-runner).")
|
||||
var executable: String?
|
||||
|
||||
func run() async throws {
|
||||
let executablePath = executable ?? LaunchdService.defaultExecutablePath
|
||||
|
||||
// Only pass --config when it is not the default; a plist that
|
||||
// hard-codes the default path is one more thing to keep in sync.
|
||||
let configPath = options.configPath == RunnerConfig.defaultPath ? nil : options.configPath
|
||||
|
||||
if (try? options.loadConfig()) == nil {
|
||||
CLI.note("warning: \(RunnerConfig.expandTilde(options.configPath)) is missing or invalid; the agent will fail to start until it is fixed")
|
||||
}
|
||||
|
||||
try LaunchdService.install(executablePath: executablePath, configPath: configPath)
|
||||
|
||||
print("installed \(LaunchdService.agentPlistURL.path)")
|
||||
print("program: \(RunnerConfig.expandTilde(executablePath)) daemon")
|
||||
print("logs: \(LaunchdService.logDirectoryURL.path)")
|
||||
print("")
|
||||
print("check it with: gitea-macos-runner service status")
|
||||
}
|
||||
}
|
||||
|
||||
/// `service uninstall` — unload and remove the plist.
|
||||
struct Uninstall: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "uninstall",
|
||||
abstract: "Unload the LaunchAgent and remove its plist."
|
||||
)
|
||||
|
||||
@OptionGroup var options: GlobalOptions
|
||||
|
||||
func run() async throws {
|
||||
let path = LaunchdService.agentPlistURL.path
|
||||
let existed = FileManager.default.fileExists(atPath: path)
|
||||
try LaunchdService.uninstall()
|
||||
print(existed ? "removed \(path)" : "not installed (\(path))")
|
||||
}
|
||||
}
|
||||
|
||||
/// `service status` — report whether the agent is installed and running.
|
||||
struct Status: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "status",
|
||||
abstract: "Report LaunchAgent installation and run state."
|
||||
)
|
||||
|
||||
@OptionGroup var options: GlobalOptions
|
||||
|
||||
func run() async throws {
|
||||
let status = try LaunchdService.status()
|
||||
|
||||
print("label: \(LaunchdService.label)")
|
||||
print("plist: \(status.plistPath)")
|
||||
print("installed: \(status.installed ? "yes" : "no")")
|
||||
print("loaded: \(status.loaded ? "yes" : "no")")
|
||||
if let pid = status.pid {
|
||||
print("pid: \(pid)")
|
||||
}
|
||||
if let lastExitStatus = status.lastExitStatus {
|
||||
print("last exit: \(lastExitStatus)")
|
||||
}
|
||||
print("logs: \(LaunchdService.logDirectoryURL.path)")
|
||||
|
||||
if status.installed, !status.loaded {
|
||||
print("")
|
||||
CLI.note("installed but not loaded — reinstall with `service install`, or check the logs above")
|
||||
throw ExitCode(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user