Merge nucleic/vivid-glass-urchin-xoym into main
This commit is contained in:
@@ -35,6 +35,16 @@ struct ServiceCommand: AsyncParsableCommand {
|
||||
@Option(name: .long, help: "Path to the installed executable (default: ~/Applications/GiteaMacosRunner.app/Contents/MacOS/gitea-macos-runner).")
|
||||
var executable: String?
|
||||
|
||||
/// How to configure Local Network access, if it is not already.
|
||||
///
|
||||
/// Unset means "decide at run time": ask on a terminal, skip with a
|
||||
/// pointer otherwise. `none` suppresses the question outright, for a
|
||||
/// scripted install that has its own arrangements.
|
||||
@Option(
|
||||
name: .customLong("grant-local-network"),
|
||||
help: "Configure macOS Local Network access during install: allowlist, prompt, or none.")
|
||||
var grantLocalNetwork: LocalNetworkGrantChoice?
|
||||
|
||||
func run() async throws {
|
||||
let executablePath = executable ?? LaunchdService.defaultExecutablePath
|
||||
|
||||
@@ -59,8 +69,81 @@ struct ServiceCommand: AsyncParsableCommand {
|
||||
print("program: \(RunnerConfig.expandTilde(executablePath)) daemon")
|
||||
print("logs: \(LaunchdService.logDirectoryURL.path)")
|
||||
print("")
|
||||
|
||||
offerLocalNetworkGrant()
|
||||
|
||||
print("check it with: gitea-macos-runner service status")
|
||||
}
|
||||
|
||||
/// Offers to configure Local Network access, if it is not already.
|
||||
///
|
||||
/// This is where the question belongs. The agent that was just
|
||||
/// installed is the process that will be blocked, it has no UI to ask
|
||||
/// with, and the symptom when it is blocked — every guest boots, no job
|
||||
/// starts, `No route to host` — points nowhere near the cause. Asking
|
||||
/// now costs one prompt; not asking costs a debugging session.
|
||||
///
|
||||
/// Never fatal: a failed or declined grant leaves a perfectly good
|
||||
/// installed agent, so this reports and returns rather than throwing.
|
||||
private func offerLocalNetworkGrant() {
|
||||
guard grantLocalNetwork != .skip else { return }
|
||||
guard !LocalNetworkPolicy.status().coversGuestRange else { return }
|
||||
|
||||
let method: LocalNetworkPermission.Method
|
||||
switch grantLocalNetwork {
|
||||
case .allowlist: method = .allowlist
|
||||
case .prompt: method = .prompt
|
||||
case .skip: return // handled above; here for exhaustiveness
|
||||
case nil:
|
||||
// Not asked for either way: decide from the terminal. A piped
|
||||
// or launchd-driven install must not stop on a question, so it
|
||||
// gets the pointer and carries on.
|
||||
guard isatty(fileno(stdin)) == 1 else {
|
||||
CLI.note("""
|
||||
note: macOS Local Network access is not configured. Until it is, guests \
|
||||
boot but SSH fails with "No route to host". Configure it with \
|
||||
`gitea-macos-runner permissions grant`.
|
||||
""")
|
||||
print("")
|
||||
return
|
||||
}
|
||||
CLI.note("""
|
||||
macOS Local Network access is not configured. Without it the agent starts \
|
||||
guests fine but cannot reach them, and every job fails with "No route to \
|
||||
host". Granting it writes a subnet allowlist with sudo and needs a reboot.
|
||||
""")
|
||||
guard CLI.confirm("configure it now?") else {
|
||||
CLI.note("skipped; run `gitea-macos-runner permissions grant` later")
|
||||
print("")
|
||||
return
|
||||
}
|
||||
method = .allowlist
|
||||
}
|
||||
|
||||
// Deliberately swallowed. The agent is installed and correct at
|
||||
// this point; a declined sudo password should not turn a successful
|
||||
// install into a failure.
|
||||
do {
|
||||
try LocalNetworkGrantFlow.run(method: method)
|
||||
} catch {
|
||||
CLI.note("could not configure it: \(error)")
|
||||
CLI.note("the agent is installed; run `gitea-macos-runner permissions grant` to retry")
|
||||
}
|
||||
print("")
|
||||
}
|
||||
}
|
||||
|
||||
/// `--grant-local-network`'s values: the two grant methods plus an explicit
|
||||
/// opt-out, which the method enum itself has no business carrying.
|
||||
///
|
||||
/// The opt-out case is spelled `skip` rather than `none` so that
|
||||
/// `choice == .skip` cannot be read as `Optional.none` — the option is
|
||||
/// itself optional, and "not passed" means something different from
|
||||
/// "passed `none`".
|
||||
enum LocalNetworkGrantChoice: String, ExpressibleByArgument, CaseIterable {
|
||||
case allowlist
|
||||
case prompt
|
||||
case skip = "none"
|
||||
}
|
||||
|
||||
/// `service uninstall` — unload and remove the plist.
|
||||
|
||||
Reference in New Issue
Block a user