mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 17:44:33 +00:00
• Strip all embedded shell from LaunchAgentManager.swift; use external etc/init-port-fowards.sh copied from app Resources at runtime; set executable perms • Set defaultHelperScript to empty to eliminate inline script • Build script: bundle init-port-fowards.sh into Prole.app/Contents/Resources and chmod +x; capture spm_build_binary path via tail -n1 • Fix malformed multiline XML in PortMappingsXML.save() • Preferences: handle new .agentPF tab in didSelect and windowDidResize Result: Prole.app builds cleanly on arm64; no inline shell remains; embedded dylibs packaged and codesigned
118 lines
5.1 KiB
Swift
118 lines
5.1 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")
|
|
}
|
|
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])
|
|
}
|
|
|
|
// Ensure the helper shell script exists by copying the external bundled script; no inline content.
|
|
func ensureHelperScript() {
|
|
let fm = FileManager.default
|
|
let dir = helperScriptURL.deletingLastPathComponent()
|
|
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
// Locate external script in app bundle Resources
|
|
let bundle = Bundle.main
|
|
let scriptInBundleURL = bundle.url(forResource: "init-port-fowards", withExtension: "sh")
|
|
guard let srcURL = scriptInBundleURL else { return }
|
|
var needsCopy = true
|
|
if fm.fileExists(atPath: helperScriptURL.path) {
|
|
if let srcAttr = try? fm.attributesOfItem(atPath: srcURL.path),
|
|
let dstAttr = try? fm.attributesOfItem(atPath: helperScriptURL.path),
|
|
let srcMod = srcAttr[.modificationDate] as? Date,
|
|
let dstMod = dstAttr[.modificationDate] as? Date,
|
|
dstMod >= srcMod {
|
|
needsCopy = false
|
|
}
|
|
}
|
|
if needsCopy {
|
|
_ = try? fm.removeItem(at: helperScriptURL)
|
|
do {
|
|
try fm.copyItem(at: srcURL, to: helperScriptURL)
|
|
} catch {
|
|
if let data = try? Data(contentsOf: srcURL) {
|
|
try? data.write(to: helperScriptURL)
|
|
}
|
|
}
|
|
try? fm.setAttributes([.posixPermissions: NSNumber(value: Int16(0o755))], ofItemAtPath: helperScriptURL.path)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|