prole/prole-tools-app/Sources/PFScriptBridge.swift

83 lines
3.9 KiB
Swift

import Foundation
// Bridge to invoke Prole Tools environment wrapper for status/restart.
// Use the environment defined in env.sh and execute scripts via $PROLE_SERVICE
// e.g. "$PROLE_SERVICE/init_port_forwards.sh <cmd>" so we don't rely on PATH.
enum PFScriptBridge {
// Keep the last command description actually used, for display in the UI.
private static var lastInvocation: String = "$PROLE_SERVICE/init_port_forwards.sh"
/// Public accessor for display purposes. Shows the last invocation we used.
static func invocationDescription() -> String { lastInvocation }
@discardableResult
static func restart() -> (code: Int32, out: String, err: String) { runScript(arg: "restart") }
static func status() -> (code: Int32, out: String, err: String) { runScript(arg: "status") }
private static func runScript(arg: String) -> (code: Int32, out: String, err: String) {
let fm = FileManager.default
let env = ProcessInfo.processInfo.environment
let home = env["HOME"] ?? NSHomeDirectory()
let proleHomeEnv = env["PROLE_HOME"]
// Candidate locations for env.sh
let envCandidates: [String] = [
(proleHomeEnv != nil ? (proleHomeEnv! + "/env.sh") : nil),
home + "/dev/prole/env.sh",
].compactMap { $0 }
// Candidate locations for the script if we can't use env.sh
let scriptCandidates: [String] = [
(proleHomeEnv != nil ? (proleHomeEnv! + "/etc/init_port_forwards.sh") : nil),
home + "/dev/prole/etc/init_port_forwards.sh",
].compactMap { $0 }
// Build the command using the best available method.
var command: String
var cwd: String = proleHomeEnv ?? home
if let envPath = envCandidates.first(where: { fm.fileExists(atPath: $0) }) {
// Use env.sh to populate PROLE_* and invoke via $PROLE_SERVICE
command = "export PROLE_HOME=\"${PROLE_HOME:-$HOME}\"; . \"\(envPath)\"; \"${PROLE_SERVICE}/init_port_forwards.sh\" \(arg)"
lastInvocation = ". \(envPath) && $PROLE_SERVICE/init_port_forwards.sh \(arg)"
} else if let scriptPath = scriptCandidates.first(where: { fm.fileExists(atPath: $0) }) {
// Fallback: directly execute the repo script
command = "\"\(scriptPath)\" \(arg)"
lastInvocation = scriptPath + " \(arg)"
} else {
// Nothing found; construct a failing but explicit command for diagnostics
command = "echo '[prole-tools] env.sh and init_port_forwards.sh not found' 1>&2; exit 127"
lastInvocation = "<not found> init_port_forwards.sh \(arg)"
}
let p = Process()
p.executableURL = URL(fileURLWithPath: "/bin/bash")
p.arguments = ["-lc", command]
p.currentDirectoryURL = URL(fileURLWithPath: cwd)
Logger.shared.info("invoking: \(lastInvocation)")
let outPipe = Pipe(); let errPipe = Pipe()
p.standardOutput = outPipe
p.standardError = errPipe
do { try p.run() } catch {
let errStr = String(describing: error)
Logger.shared.error("spawn error: \(errStr)")
return (-1, "", errStr)
}
p.waitUntilExit()
let out = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let err = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let code = p.terminationStatus
// Combine to match UI exactly
var combined = out
if !err.isEmpty { combined += (combined.isEmpty ? "" : "\n") + err }
let trimmedCombined = combined.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedCombined.isEmpty {
Logger.shared.info("exit code: \(code); (no output)")
} else {
Logger.shared.info("output (exit=\(code)):\n\(combined)")
}
return (code, out, err)
}
}