import Foundation enum MediaToolLocator { static func executable(named name: String) -> URL? { guard let bundled = Bundle.main.resourceURL?.appendingPathComponent(name), FileManager.default.isExecutableFile(atPath: bundled.path) else { return nil } return bundled } static func javascriptRuntimeArguments() -> [String] { guard let runtime = executable(named: "deno") else { return [] } return ["--js-runtimes", "deno:\(runtime.path)"] } } enum MediaToolError: LocalizedError { case missing(String) case failed(String, String) var errorDescription: String? { switch self { case .missing(let tool): "\(tool) fehlt im App-Paket. Installiere eine App-Version mit gebündelten Werkzeugen." case .failed(let tool, let detail): "\(tool) ist fehlgeschlagen: \(detail)" } } } enum MediaProcess { static func run(_ executable: URL, arguments: [String]) throws -> Data { let id = UUID().uuidString let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("tonuino-tool-\(id).out") let errorURL = FileManager.default.temporaryDirectory.appendingPathComponent("tonuino-tool-\(id).err") FileManager.default.createFile(atPath: outputURL.path, contents: nil) FileManager.default.createFile(atPath: errorURL.path, contents: nil) defer { try? FileManager.default.removeItem(at: outputURL) try? FileManager.default.removeItem(at: errorURL) } let outputHandle = try FileHandle(forWritingTo: outputURL) let errorHandle = try FileHandle(forWritingTo: errorURL) defer { try? outputHandle.close(); try? errorHandle.close() } let process = Process() process.executableURL = executable process.arguments = arguments process.standardOutput = outputHandle process.standardError = errorHandle do { try process.run() } catch { throw MediaToolError.failed(executable.lastPathComponent, error.localizedDescription) } process.waitUntilExit() let output = try Data(contentsOf: outputURL) let errorOutput = String(data: try Data(contentsOf: errorURL), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard process.terminationStatus == 0 else { let detail = errorOutput.isEmpty ? "Prozesscode \(process.terminationStatus)" : String(errorOutput.suffix(1800)) throw MediaToolError.failed(executable.lastPathComponent, detail) } return output } }