250 lines
8.0 KiB
Swift
250 lines
8.0 KiB
Swift
//
|
|
// 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)
|
|
|
|
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]) {
|
|
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, 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)
|
|
}
|
|
}
|
|
}
|