prole/prole-tools-app/Sources/LaunchAgentManager.swift
chrisfu 0e01595de0 refactor: switch to environment wrapper; standardize env.sh usage and improve $PROLE_HOME management
- Replace direct script invocation with `$PROLE_HOME/env.sh` wrapper for consistent environment setup across all components.
- Update runtime processes to dynamically resolve `$PROLE_HOME` or fallback to `$HOME/.prole`.
- Deprecate `init-port-forwards.sh` script bundling; remove from LaunchAgentManager and build system.
- Overhaul `env.sh` generation to include execution capability, customizable paths, and improved error handling.
- Standardize app name and paths to "Prole Tools" across all files and UI elements.
- Adjust build output to align with new structure, including executable naming and resource packaging.
2025-12-15 22:00:32 -08:00

91 lines
4.0 KiB
Swift

import Foundation
// Lightweight helper to manage the user LaunchAgent that runs kubectl port-forwards.
// This replaces the in-app PortForwardManager supervision.
final class LaunchAgentManager {
// Filenames/paths
private let label = "org.prole.prole-db.kpf-dev"
private var plistURL: URL {
let home = FileManager.default.homeDirectoryForCurrentUser
return home.appendingPathComponent("Library/LaunchAgents/\(label).plist")
}
// Deprecated: helper script is no longer bundled. All shell commands must go via $PROLE_HOME/env.sh.
// Leaving the placeholder to avoid breaking callers; it is unused now.
private var helperScriptURL: URL {
let home = FileManager.default.homeDirectoryForCurrentUser
return home.appendingPathComponent("Library/Application Support/Prole/bin/prole-kpf.sh")
}
// Key that stores the list of commands inside the plist (array of strings)
private let commandsKey = "ProleCommands"
// Read commands from the LaunchAgent plist. Returns empty if missing.
func readCommands() -> [String] {
guard let data = try? Data(contentsOf: plistURL) else { return [] }
var format = PropertyListSerialization.PropertyListFormat.xml
guard let obj = try? PropertyListSerialization.propertyList(from: data, options: [], format: &format),
let dict = obj as? [String: Any],
let cmds = dict[commandsKey] as? [String] else {
return []
}
return cmds
}
// Write commands back to the plist, preserving other keys if present.
func writeCommands(_ commands: [String]) {
var dict: [String: Any] = [:]
if let data = try? Data(contentsOf: plistURL),
let obj = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil),
let existing = obj as? [String: Any] {
dict = existing
}
dict[commandsKey] = commands
if let out = try? PropertyListSerialization.data(fromPropertyList: dict, format: .xml, options: 0) {
// Ensure parent directory exists
try? FileManager.default.createDirectory(at: plistURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try? out.write(to: plistURL)
}
}
// Reload the LaunchAgent: try kickstart; if not present, bootstrap then kickstart.
func resetAgent() {
let uid = getuid()
let domainTarget = "gui/\(uid)/\(label)"
// Try kickstart first (reload)
let ks = run("/bin/launchctl", ["kickstart", "-k", domainTarget])
if ks.exitCode == 0 { return }
// If kickstart failed, try bootout then bootstrap, then kickstart again
_ = run("/bin/launchctl", ["bootout", "gui/\(uid)", domainTarget])
// bootstrap requires path to plist
_ = run("/bin/launchctl", ["bootstrap", "gui/\(uid)", plistURL.path])
_ = run("/bin/launchctl", ["kickstart", "-k", domainTarget])
}
// Deprecated: No-op. We no longer bundle or copy any init-port-forwards.sh; app uses $PROLE_HOME/env.sh.
func ensureHelperScript() { /* no-op */ }
static let defaultHelperScript = ""
// MARK: - Helpers
@discardableResult
private func run(_ path: String, _ args: [String]) -> (exitCode: Int32, out: String, err: String) {
let p = Process()
p.executableURL = URL(fileURLWithPath: path)
p.arguments = args
let outPipe = Pipe(); let errPipe = Pipe()
p.standardOutput = outPipe
p.standardError = errPipe
do { try p.run() } catch {
return (exitCode: -1, out: "", err: String(describing: error))
}
p.waitUntilExit()
let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
let outStr = String(data: outData, encoding: .utf8) ?? ""
let errStr = String(data: errData, encoding: .utf8) ?? ""
return (p.terminationStatus, outStr, errStr)
}
}