1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 16:14:44 +00:00
Files
Mole/app/ShellRunner.swift

135 lines
4.7 KiB
Swift

import Foundation
enum ShellError: Error, LocalizedError {
case commandFailed(output: String)
case executionError(error: Error)
case authenticationFailed
var errorDescription: String? {
switch self {
case .commandFailed(let output): return output
case .executionError(let error): return error.localizedDescription
case .authenticationFailed: return "Authentication failed - incorrect password"
}
}
}
class ShellRunner {
static let shared = ShellRunner()
private init() {}
/// Runs a shell command as the current user
func run(_ command: String, arguments: [String] = []) async throws -> String {
let process = Process()
let pipe = Pipe()
let errorPipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [command] + arguments
process.standardOutput = pipe
process.standardError = errorPipe
return try await withCheckedThrowingContinuation { continuation in
process.terminationHandler = { process in
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
// Also capture error output
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let errorOutput = String(data: errorData, encoding: .utf8) ?? ""
if process.terminationStatus == 0 {
continuation.resume(returning: output)
} else {
// Combine stdout and stderr for debugging
continuation.resume(
throwing: ShellError.commandFailed(output: output + "\n" + errorOutput))
}
}
do {
try process.run()
} catch {
continuation.resume(throwing: ShellError.executionError(error: error))
}
}
}
/// Runs a full shell command string (e.g. involving pipes or multiple args)
func runShell(_ command: String) async throws -> String {
return try await run("bash", arguments: ["-c", command])
}
/// Runs a command with Administrator privileges using AppleScript
/// Note: This will trigger the system permission dialog
func runAdmin(_ command: String) async throws -> String {
// Escape quotes and backslashes for AppleScript string to prevent syntax errors
let escapedCommand =
command
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
let appleScript = "do shell script \"\(escapedCommand)\" with administrator privileges"
return try await run("osascript", arguments: ["-e", appleScript])
}
/// Runs a command with sudo using a provided password (via stdin)
func runSudo(_ command: String, password: String) async throws -> String {
let process = Process()
let pipe = Pipe()
let errorPipe = Pipe()
let inputPipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
// -S reads password from stdin, -p '' disables the prompt string
// We wrap the actual command in bash -c to handle complex strings
process.arguments = ["-S", "-p", "", "bash", "-c", command]
process.standardOutput = pipe
process.standardError = errorPipe
process.standardInput = inputPipe
return try await withCheckedThrowingContinuation { continuation in
process.terminationHandler = { process in
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let errorOutput = String(data: errorData, encoding: .utf8) ?? ""
if process.terminationStatus == 0 {
continuation.resume(returning: output)
} else {
// Check for password failure
let combined = (output + errorOutput).lowercased()
if combined.contains("try again") || combined.contains("incorrect") {
continuation.resume(throwing: ShellError.authenticationFailed)
} else {
continuation.resume(
throwing: ShellError.commandFailed(output: errorOutput.isEmpty ? output : errorOutput)
)
}
}
}
do {
try process.run()
// Write password to stdin
if let passData = (password + "\n").data(using: .utf8) {
try? inputPipe.fileHandleForWriting.write(contentsOf: passData)
try? inputPipe.fileHandleForWriting.close()
}
} catch {
continuation.resume(throwing: ShellError.executionError(error: error))
}
}
}
/// Escapes a string for safe use in bash commands
private func bashEscape(_ str: String) -> String {
// Use single quotes and escape any single quotes within the string
let escaped = str.replacingOccurrences(of: "'", with: "'\"'\"'")
return "'\(escaped)'"
}
}