Compare commits

...

1 Commits

Author SHA1 Message Date
Jake Hillion
b4891f72a3 EXO: add CLI flags for root install/uninstall
The macOS app required user interaction via AppleScript prompts to
install or uninstall network configuration components, making automated
deployments difficult.

Added --install and --uninstall command line flags that execute the
network setup scripts directly when running as root, bypassing GUI
prompts. Created a new main.swift entry point that parses CLI arguments
and delegates to NetworkSetupHelper's new direct execution methods.

This enables headless installation via `sudo EXO --install` for
automated deployment scenarios while preserving the existing GUI
behavior when launched normally.

Test plan:

- Deployed to a machine that didn't have the content installed. Got
  blocked on the popup and EXO never launched.
- Relaunched EXO, confirmed it still never starts because of the popup.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --install`
- Launched EXO - the API started as expected.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --uninstall`
- Launched EXO - got the popup.
2026-02-04 13:22:02 +00:00
3 changed files with 140 additions and 1 deletions

View File

@@ -14,7 +14,6 @@ import SwiftUI
import UserNotifications
import os.log
@main
struct EXOApp: App {
@StateObject private var controller: ExoProcessController
@StateObject private var stateService: ClusterStateService

View File

@@ -288,6 +288,61 @@ enum NetworkSetupHelper {
"""
}
/// Direct install without GUI (requires root).
/// Returns true on success, false on failure.
static func installDirectly() -> Bool {
let script = makeInstallerScript()
return runShellDirectly(script)
}
/// Direct uninstall without GUI (requires root).
/// Returns true on success, false on failure.
static func uninstallDirectly() -> Bool {
let script = makeUninstallScript()
return runShellDirectly(script)
}
/// Run a shell script directly via Process (no AppleScript, requires root).
/// Returns true on success, false on failure.
private static func runShellDirectly(_ script: String) -> Bool {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", script]
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
do {
try process.run()
process.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: outputData, encoding: .utf8), !output.isEmpty {
print(output)
}
if let errorOutput = String(data: errorData, encoding: .utf8), !errorOutput.isEmpty {
fputs(errorOutput, stderr)
}
if process.terminationStatus == 0 {
logger.info("Shell script completed successfully")
return true
} else {
logger.error("Shell script failed with exit code \(process.terminationStatus)")
return false
}
} catch {
logger.error(
"Failed to run shell script: \(error.localizedDescription, privacy: .public)")
fputs("Error: \(error.localizedDescription)\n", stderr)
return false
}
}
private static func runShellAsAdmin(_ script: String) throws {
let escapedScript =
script

85
app/EXO/EXO/main.swift Normal file
View File

@@ -0,0 +1,85 @@
//
// main.swift
// EXO
//
// Created by Jake Hillion on 2025-02-03.
//
import Foundation
/// Command line options for the EXO app
enum CLICommand {
case install
case uninstall
case help
case none
}
/// Parse command line arguments to determine the CLI command
func parseArguments() -> CLICommand {
let args = CommandLine.arguments
if args.contains("--help") || args.contains("-h") {
return .help
}
if args.contains("--install") {
return .install
}
if args.contains("--uninstall") {
return .uninstall
}
return .none
}
/// Print usage information
func printUsage() {
let programName = (CommandLine.arguments.first as NSString?)?.lastPathComponent ?? "EXO"
print(
"""
Usage: \(programName) [OPTIONS]
Options:
--install Install EXO network configuration (requires root)
--uninstall Uninstall EXO network configuration (requires root)
--help, -h Show this help message
When run without options, starts the normal GUI application.
Examples:
sudo \(programName) --install Install network components as root
sudo \(programName) --uninstall Remove network components as root
""")
}
/// Check if running as root
func isRunningAsRoot() -> Bool {
return getuid() == 0
}
// Main entry point
let command = parseArguments()
switch command {
case .help:
printUsage()
exit(0)
case .install:
if !isRunningAsRoot() {
fputs("Error: --install requires root privileges. Run with sudo.\n", stderr)
exit(1)
}
let success = NetworkSetupHelper.installDirectly()
exit(success ? 0 : 1)
case .uninstall:
if !isRunningAsRoot() {
fputs("Error: --uninstall requires root privileges. Run with sudo.\n", stderr)
exit(1)
}
let success = NetworkSetupHelper.uninstallDirectly()
exit(success ? 0 : 1)
case .none:
// Start normal GUI application
EXOApp.main()
}