// // KissMatrix.swift // KissMeMatrix // // Created by ened-book-m1 on 2023/06/20. // import Foundation import KissMe class KissMatrix { func run() { func printUsage() { let appName = (CommandLine.arguments[0] as NSString).lastPathComponent printError("\(appName) [sim|run] model.json [yyyyMMdd] [HHmmss]") } guard CommandLine.argc >= 3 else { printUsage() return } let runDate: Date let mode = RunMode(rawValue: CommandLine.arguments[1]) switch mode { case .runner: runDate = Date() case .simulator: guard CommandLine.argc == 5 else { printUsage() return } guard let (year, month, day) = CommandLine.arguments[3].yyyyMMdd else { printError("Invalid [yyyyMMdd] argument") printUsage() return } guard let (hour, min, sec) = CommandLine.arguments[4].HHmmss else { printError("Invalid [HHmmss] argument") printUsage() return } var date = Date(timeIntervalSince1970: 0) date.change(year: year, month: month, day: day) date.change(hour: hour, min: min, sec: sec) runDate = date default: printUsage() return } let modelJson = CommandLine.arguments[2] guard let model = loadModel(modelJson) else { return } printError("Loaded \(model.indexSets.count) index set from \(modelJson)") let semaphore = DispatchSemaphore(value: 0) Task { let results = try await withThrowingTaskGroup(of: KissIndexResult?.self, returning: [KissIndexResult].self) { taskGroup in for indexSet in model.indexSets { guard let (runUrl, args) = indexSet.build(date: runDate) else { printError("Cannot get command from \(indexSet.name)") continue } taskGroup.addTask { do { let result = try await self.runIndex(runUrl, args: args, date: runDate) return result } catch { printError(error) return nil } } } var taskResult = [KissIndexResult]() for try await result in taskGroup.compactMap( { $0 }) { taskResult.append(result) } return taskResult } mergeResult(results, date: runDate) semaphore.signal() } semaphore.wait() } private func runIndex(_ runUrl: URL, args: [String], date: Date) async throws -> KissIndexResult { assert(args.count >= 3) return try await withUnsafeThrowingContinuation { continuation in let kmi = args[0] let outputUrl = KissMatrix.indexLogFile(kmi, date: date) FileManager.default.createFile(atPath: outputUrl.path, contents: nil) let output = FileHandle(forWritingAtPath: outputUrl.path) let task = Process() task.currentDirectoryURL = URL.currentDirectory() task.executableURL = runUrl task.arguments = args task.standardOutput = output task.standardError = FileHandle.standardError task.qualityOfService = .default printError("curPath: \(task.currentDirectoryPath)") printError("runPath: \(runUrl.path)") printError("args: \(args)") do { try task.run() task.waitUntilExit() } catch { printError("run error \(error)") continuation.resume(throwing: error) } try? output?.close() guard let data = try? Data(contentsOf: outputUrl) else { continuation.resume(throwing: GeneralError.emptyData(kmi)) return } if data.isEmpty { continuation.resume(throwing: GeneralError.emptyData(kmi)) return } do { let indexResult = try JSONDecoder().decode(KissIndexResult.self, from: data) continuation.resume(returning: indexResult) } catch { printError("jsonError \(kmi)") continuation.resume(throwing: error) } } } private func mergeResult(_ results: [KissIndexResult], date: Date) { let indexCount = results.count var mergedOutput = [String: [KissIndexResult.Output]]() for result in results { for item in result.output { if let _ = mergedOutput[item.shortCode] { mergedOutput[item.shortCode]!.append(item) } else { mergedOutput[item.shortCode] = [item] } } } var normalized = [KissIndexResult.Output]() for (productNo, output) in mergedOutput { let weight = output.reduce(0.0, { $0 + $1.weight }) / Double(indexCount) let output = KissIndexResult.Output(shortCode: productNo, productName: output.first?.productName, weight: weight) normalized.append(output) } normalized.sort(by: { $0.weight > $1.weight }) let kmis = results.map { $0.kmi } let matrixResult = KissMatrixResult(code: 200, kmis: kmis, day: date.yyyyMMdd, time: date.HHmmss, output: normalized) do { let jsonData = try JSONEncoder().encode(matrixResult) try FileHandle.standardOutput.write(contentsOf: jsonData) } catch { printError(error) } } private func loadModel(_ jsonFile: String) -> Model? { do { let configUrl = URL.currentDirectory().appending(path: jsonFile) let data = try Data(contentsOf: configUrl, options: .uncached) let model = try JSONDecoder().decode(Model.self, from: data) return model } catch { printError(error) return nil } } static func indexLogFile(_ kmi: String, date: Date) -> URL { let subPath = "log/index" let subFile = "\(subPath)/\(date.yyyyMMdd_HHmmssSSSS_forFile)-\(kmi).log" let fileUrl = URL.currentDirectory().appending(path: subFile) createSubpath(subPath) return fileUrl } static func createSubpath(_ name: String) { let subPath = URL.currentDirectory().appending(path: name) try? FileManager.default.createDirectory(at: subPath, withIntermediateDirectories: true) } } enum RunMode: String { case simulator = "sim" case runner = "run" } struct Model: Decodable { let indexSets: [IndexSet] struct IndexSet: Codable { /// Index Set 이름 let name: String /// Index Set 에 대한 설명 let memo: String /// config.json 설정 let config: String? /// Index 수집을 수행하는 Runner app let runner: String /// 보정용 가중치 let weight: Double func build(date: Date) -> (URL, [String])? { guard let _ = name.kmiIndex else { return nil } let command = URL.currentDirectory().appending(path: runner) let day = date.yyyyMMdd let time = date.HHmmss var args: [String] = [name, day, time] if let config = config { args.append(config) } return (command, args) } } }