prole/prole-app/Sources/LaunchAgentManager.swift
chrisfu 197c72dd7a Refactor prole-app and establish temporary release process
- Moved prole-tools-app to prole-app at the project root to make it self-contained for transition to its own repository.
- Created prole-tools-app/dist/ directory to host build artifacts.
- Generated distribution artifacts (Prole Tools.app and Prole Tools.zip) using prole-app/build.sh package.
- Checked in the generated artifacts to prole-tools-app/dist/ (bypassing .gitignore for temporary release process).
Changes Summary
•
Renamed directory prole-tools-app/ to prole-app/.
•
Populated prole-tools-app/dist/ with the latest build output from prole-app/build.sh.
•
Staged all changes, including the forced addition of ignored artifacts in prole-tools-app/dist/.
2026-01-18 15:04:23 -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)
}
}