/* * Copyright (c) 2022 LG Electronics Inc. * SPDX-License-Identifier: LicenseRef-LGE-Proprietary */ using Aurora; using Aurora.Api; using Aurora.Api.DataType; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.UI.WebControls; using System.Windows; namespace AuroraUIComponent.Sequence { public class GlassInLogItem { public int RecipeId{ get; set; } public string RecipeName { get; set; } public int LayerNum { get; set; } public string Desc { get; set; } //public bool IsAging { get; set; } //public int OrgRecipeIdFromPLC { get; set; } //public int OrgLayerNumdFromPLC { get; set; } public GlassInLogItem(int recipeId, string recipeName, int layerNum, string desc/*, bool isAging = false, int orgRecipeIdFromPLC = -1, int orgLayerNumFromPLC = -1*/) { RecipeId = recipeId; RecipeName = recipeName; LayerNum = layerNum; Desc = desc; //IsAging = isAging; //OrgRecipeIdFromPLC = orgRecipeIdFromPLC; //OrgLayerNumdFromPLC = orgLayerNumFromPLC; } } public class CsvLogSequence { static readonly Logger Log = new Logger(nameof(CsvLogSequence)); /// /// Param 1 : ProcLogId, 2: BeforeFile, 3: NewFile /// public const string EVENT_NOTIFY_LOG_FILE_CHANGED = "EVENT_NOTIFY_LOG_FILE_CHANGED"; // file open 시 알림 /// /// Log Event (헤더 추가) Publish /// public const string EVENT_NOTIFY_HEADER_ADDED = "EVENT_NOTIFY_HEADER_ADDED"; /// /// Log Event (로그 추가) Publish /// public const string EVENT_NOTIFY_LOG_ADDED = "EVENT_NOTIFY_LOG_ADDED"; private static object _lock = new object(); #region 시퀀스 노드 이름 const string SEQ_Init = "Init"; // 딱 한번 Sequence 진입 시에만 실행될 Node const string SEQ_Wait = "Wait"; const string SEQ_CheckNewFile = "CheckNewFile"; const string SEQ_CheckNewLogData = "CheckNewLogData"; const string SEQ_CheckSettingUpdated = "CheckSettingUpdated"; #endregion public static SequenceBuilder Builder() { SequenceBuilder sb = new SequenceBuilder(1000, true); // 무한, 시퀀스 Stop시 최대 대기시간. sb.AddAction(SEQ_Init, SeqInitNode); sb.AddAction(SEQ_Wait, SeqWait); sb.AddAction(SEQ_CheckNewFile, SeqCheckNewFile); sb.AddAction(SEQ_CheckNewLogData, SeqCheckNewLogData); sb.AddDecision(SEQ_CheckSettingUpdated, SeqCheckSettingUpdated); sb.Flows(sb.Init > sb[SEQ_Init] > sb[SEQ_Wait] > sb[SEQ_CheckNewFile] > sb[SEQ_CheckNewLogData] > sb[SEQ_CheckSettingUpdated]); sb.Flows(sb[SEQ_CheckSettingUpdated] > sb[SEQ_Wait]); sb.Flows(sb[SEQ_CheckSettingUpdated] > sb[SEQ_Init]); sb.OnStopped(OnStopped); sb.OnForceError(OnForceError); //sb.Draw(nameof(CsvLogSequence) + ".pdf"); return sb; } private static string SeqCheckSettingUpdated(SequenceContext context, string id, ICollection branches, SequenceActionResult param) { if (NeedUpdate) { return SEQ_Init; } return SEQ_Wait; } private static bool NeedUpdate = false; private static string SettingId = null; public static void UpdateSetting(string settingId) { NeedUpdate = true; SettingId = settingId; } public class CsvLogItemEx : CsvLogSetting.LogItem { public DateTime CurLogFileTime { get; internal set; } public string CurLogFileName { get; internal set; } public string CurLogFileFullPath { get; internal set; } public string CurLogDir { get; set; } public int CurLogFileIndex { get; internal set; } public Queue> ItemToSave = new Queue>(); public FileStream FStream { get; internal set; } public StreamWriter FStreamWriter { get; internal set; } public bool IsHeaderWritten { get; internal set; } = false; public int LogCount { get; set; } = 0; public DateTime TimeResetCheckStartTime; private string CreateFileName(DateTime dt, string prefix, string postfix, int fileNumber) { return (string.IsNullOrEmpty(prefix) ? "" : prefix + "_") + dt.ToString("yyMMdd-HHmmss") + (string.IsNullOrEmpty(postfix) ? "" : "_" + postfix) + (fileNumber != 0 ? ("." + fileNumber) : string.Empty) + ".csv"; } internal string CheckAndUpdateFileName(bool initialCheck = false) { this.CurLogFileTime = DateTime.Now; bool makeNewName = true; string _LogFileFullPath; // ReUseFileIfExist : 계속 appen해서 저장해야 한다는 요구사항(AlignResult)을 위한 코드 var lastFileName = Directory.GetFiles(CurLogDir).OrderBy(file => file).LastOrDefault(); if (initialCheck && ReUseFileIfExist && !string.IsNullOrEmpty(lastFileName)) { var lastLogFileFullPath = Path.Combine(CurLogDir, lastFileName); if (File.Exists(lastLogFileFullPath)) { int logCount = CalculateLineCount(lastFileName); if (logCount < MaxLogCount) { this.CurLogFileName = lastFileName; makeNewName = false; Log.W($"ReUse Exist File :: Log[{this.Name}] File[{CurLogFileName}] :: Start from Line[{logCount}]"); } } } if (makeNewName) { var newFileTime = DateTime.Now; if (newFileTime.ToString("yyMMdd-HHmmss") != this.CurLogFileTime.ToString("yyMMdd-HHmmss")) { this.CurLogFileIndex = 0; } this.CurLogFileName = CreateFileName(this.CurLogFileTime, this.Prefix, this.Postfix, this.CurLogFileIndex); // 이미 해당 파일이 있을 때를 위한 Index++ _LogFileFullPath = Path.Combine(CurLogDir, this.CurLogFileName); if (File.Exists(_LogFileFullPath)) { while (File.Exists(_LogFileFullPath)) { this.CurLogFileIndex++; if (CurLogFileIndex > 100) { // 일단 개발 중 방어코드. return string.Empty; } this.CurLogFileName = CreateFileName(this.CurLogFileTime, this.Prefix, this.Postfix, this.CurLogFileIndex); _LogFileFullPath = Path.Combine(CurLogDir, this.CurLogFileName); } } } _LogFileFullPath = Path.Combine(CurLogDir, this.CurLogFileName); CurLogFileFullPath = _LogFileFullPath; return CurLogFileFullPath; } internal int CalculateLineCount(string filePath) { int lineCount = 0; if (File.Exists(filePath)) { using (var reader = new StreamReader(filePath)) { while (reader.ReadLine() != null) { lineCount++; } } } return lineCount; } internal void MakeFileStream() { LogCount = CalculateLineCount(this.CurLogFileFullPath); if (LogCount > 0) { Log.I($"Log[{this.Name}] File[{CurLogFileFullPath}] :: Start from Line[{LogCount}]"); LogCount--; // Header까지 한줄 추가되어 있는 상태이므로 빼기 } this.FStream = new FileStream(this.CurLogFileFullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite); this.FStream.Seek(0, SeekOrigin.End); // Move the stream position to the end of the file this.FStreamWriter = new StreamWriter(this.FStream); this.FStreamWriter.AutoFlush = true; IsHeaderWritten = (LogCount > 0); TimeResetCheckStartTime = DateTime.Now; } /// /// 헤더 추가 /// /// DateTime 포함된 ValueTuple public string InsertHeader(ValueTuple obj) { if (this.FStream == null) return null; if (this.FStreamWriter == null) return null; string headerLine = null; if (obj.Item3 != null) { FStreamWriter.Write("DateTime,"); headerLine = "DateTime,"; foreach (var prop in obj.Item3.GetType().GetProperties()) { FStreamWriter.Write(prop.Name); FStreamWriter.Write(","); headerLine = headerLine + prop.Name + ","; } FStreamWriter.WriteLine(""); IsHeaderWritten = true; } // 헤더를 넣을 때 (= 첫 데이터를 넣을 때) 시간 리셋 TimeResetCheckStartTime = DateTime.Now; return headerLine; } /// /// 로그 내용 한 줄 Write /// /// LogItem (DateTime 포함된 ValueTuple 아님) /// 로그 Timestamp internal string WriteLog(object obj, DateTime dateTime) { if (this.FStream == null) return null; if (this.FStreamWriter == null) return null; string headerLine = null; if (obj != null) { FStreamWriter.Write(dateTime.ToString("yyyy-MM-dd HH:mm:ss.fff,")); headerLine = dateTime.ToString("yyyy-MM-dd HH:mm:ss.fff,"); foreach (var prop in obj.GetType().GetProperties()) { FStreamWriter.Write(prop.GetValue(obj)); FStreamWriter.Write(","); headerLine = headerLine + prop.GetValue(obj) + ","; } FStreamWriter.WriteLine(""); LogCount++; return headerLine; } return null; } internal void CloseFileStream() { if (this.FStreamWriter != null) { this.FStreamWriter.Close(); } if (this.FStream != null) { this.FStream.Close(); } //if (!string.IsNullOrEmpty(this.CurLogFileName) && !string.IsNullOrEmpty(this.CurLogFileFullPath) && File.Exists(this.CurLogFileFullPath)) { // if (File.ReadAllText(this.CurLogFileFullPath).Length <= 0) { // // Empty File 삭제 // File.Delete(this.CurLogFileFullPath); // } //} if (!this.Enable) return; if (this.CurLogFileIndex >= 0) { this.CurLogFileIndex++; } } internal long GetFileSizeInByte() { if (FStream != null) { return new FileInfo(FStream.Name).Length; // Byte 단위 } return -1; } internal long GetFileSizeInMB() { if (FStream != null) { return new FileInfo(FStream.Name).Length / 1024 / 1024; // MB 단위 } return -1; } internal bool CheckDir() { string _LogDir; // DataPath (Recipe 경로) 아래에 넣지 않을 수도 있도록 절대경로 확인 if (this.LogDir.Contains(":\\")) { var driveName = this.LogDir.Split(':').FirstOrDefault(); if (!string.IsNullOrWhiteSpace(driveName) && DriveInfo.GetDrives().Any(drive => drive.IsReady && drive.Name == (driveName + ":\\"))) { // 정상 드라이브. Full Path 그대로 사용 _LogDir = this.LogDir; } else { // 비정상 케이스 Log.FuncE("Invalid Directory : " + this.LogDir); return false; } } else { if (!string.IsNullOrEmpty(ApplicationExt.DataPath)) { _LogDir = Path.Combine(ApplicationExt.DataPath, this.LogDir); } else { _LogDir = this.LogDir; // out 폴더 밑에 } } if (string.IsNullOrEmpty(_LogDir)) { return false; } if (!Directory.Exists(_LogDir)) { Directory.CreateDirectory(_LogDir); } if (!Directory.Exists(_LogDir)) { Log.E("Something Wrong :: Cannot Create Directory"); this.Enable = false; } if (!this.Enable) return false; this.CurLogDir = _LogDir; return true; } } private static List Logs = new List(); private static List OpenFileList = new List(); private static SequenceActionResult SeqInitNode(SequenceContext context, string id, SequenceActionResult param) { NeedUpdate = false; Log.FuncI("enter"); if (string.IsNullOrEmpty(SettingId)) return SequenceAction.Ok(); var setting = context.Settings.Get(SettingId) as CsvLogSetting; if (setting == null) return SequenceAction.Ok(); lock (_lock) { // CsvLogSetting이 바뀌어서 Sequence를 초기화할 때 : 기존에 열려있던 데이터 close foreach (var logItem in Logs) { logItem.CloseFileStream(); } // 기본 세팅 복사 foreach (var logSetting in setting.Items) { if (!Logs.Any(x => x.Name == logSetting.Name)) { Logs.Add(new CsvLogItemEx() { Name = logSetting.Name }); } var logItem = Logs.FirstOrDefault(x => x.Name == logSetting.Name); logItem.Copy(logSetting); } // 이미 Add되어 있던 LogItem(CsvLogItemEx)이 없어졌더라도 삭제하지 않고 종료시까지 유지 // 중복체크 for (int i = 0; i < Logs.Count(); i++) { if (Logs.Take(i).Any(x => x.LogDir == Logs[i].LogDir)) { // Dir이 같게 설정된 폴더가 있음 : Disable Log.E($"Duplicated LogDir :: Check [{Logs[i].Name}] Setting !!"); Logs[i].Enable = false; } } foreach (var logItem in Logs) { if (!logItem.Enable) continue; if (string.IsNullOrEmpty(logItem.LogDir)) { logItem.Enable = false; } if (!logItem.Enable) continue; // 폴더 체크 및 생성 if (logItem.CheckDir() == false) { Log.E($"Invalid LogDir :: Check #[{logItem.Name}] Setting !!"); continue; } // 로그파일 이름 생성 (이미 존재할 경우 INDEX ++, ReUse option 켜져있으면 이전 파일 다시 사용) logItem.CheckAndUpdateFileName(initialCheck: true); if (string.IsNullOrEmpty(logItem.CurLogFileFullPath)) { Log.E("Something Wrong :: Cannot Create Full Path"); logItem.Enable = false; } if (!logItem.Enable) continue; // 파일스트림 OPEN logItem.MakeFileStream(); OpenFileList.Add(logItem.CurLogFileFullPath); Log.D("Make LogFile : " + logItem.CurLogFileFullPath); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_LOG_FILE_CHANGED, (logItem.Name, string.Empty, logItem.CurLogFileName))); } } return SequenceAction.Ok(); } private static SequenceActionResult SeqWait(SequenceContext context, string id, SequenceActionResult param) { // 몇초마다 flush할 지 조정 Thread.Sleep(1000); return SequenceAction.Ok(); } /// /// CEqpSeq::InterlockProcess() 포팅 /// /// /// /// /// private static SequenceActionResult SeqCheckNewFile(SequenceContext context, string id, SequenceActionResult param) { Thread.Sleep(10); lock (_lock) { foreach (var logItem in Logs) { if (!logItem.Enable) continue; if (File.Exists(logItem.CurLogFileFullPath)) { bool needToChange = false; // 새 파일로 변경 필요한 경우 if (logItem.MaxFileSizeInMB > 0 && logItem.GetFileSizeInMB() >= logItem.MaxFileSizeInMB) { Log.FuncV($"Make New File Because of MaxFileSizeInMB : {logItem.GetFileSizeInMB()} >= {logItem.MaxFileSizeInMB}"); needToChange = true; } else if (logItem.MaxLogCount > 0 && logItem.LogCount >= logItem.MaxLogCount) { Log.FuncV($"Make New File Because of MaxLogCount : {logItem.LogCount} >= {logItem.MaxLogCount})"); needToChange = true; } else if (logItem.MaxTimeIntervalInMinute > 0) { var curTime = DateTime.Now; TimeSpan timeDifference = curTime - logItem.TimeResetCheckStartTime; if (timeDifference.TotalSeconds / 60 >= logItem.MaxTimeIntervalInMinute) { if (logItem.LogCount > 0) { // 해당 시간 동안 쌓인 데이터가 있었다면 로그 파일 change (+ Publish Event) needToChange = true; Log.FuncV($"Make New File Because of New Log[{logItem.LogCount}] While : {timeDifference.TotalSeconds / 60} >= {logItem.MaxTimeIntervalInMinute})"); } else { // 이 시점까지 로그가 없었다면 타이머를 Reset logItem.TimeResetCheckStartTime = curTime; } } } if (needToChange) { // 파일스트림 CLOSE logItem.CloseFileStream(); string lastFileName = logItem.CurLogFileName; // 로그파일 이름 생성 (이미 존재할 경우 INDEX ++) logItem.CheckAndUpdateFileName(); if (string.IsNullOrEmpty(logItem.CurLogFileFullPath)) { Log.E("Something Wrong :: Cannot Create Full Path"); logItem.Enable = false; } if (!logItem.Enable) continue; // 파일스트림 OPEN logItem.MakeFileStream(); OpenFileList.Add(logItem.CurLogFileFullPath); string curFileName = logItem.CurLogFileName; Log.W("Make LogFile : " + logItem.CurLogFileFullPath); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_LOG_FILE_CHANGED, (logItem.Name, lastFileName, curFileName))); } } } } return SequenceAction.Ok(); } private static Stopwatch _sw = new Stopwatch(); private static SequenceActionResult SeqCheckNewLogData(SequenceContext context, string id, SequenceActionResult param) { Thread.Sleep(10); // 마그넷조립기 성능개선 : Main Sequence 구동 중에는 CSV Log Write 미루기 => Main Sequence 구동 시나리오에 따라 다르므로 주석처리 //if (context.Sequences.GetSequences().Where(x => x.SequenceId.Contains("Main")).Any(x => x.RunState == SequenceRunState.Running)) { // return SequenceAction.Ok(); //} lock (_lock) { foreach (var logItem in Logs) { if (!logItem.Enable) continue; if (logItem.ItemToSave == null || logItem.ItemToSave.Count == 0) continue; Log.FuncI($"[{logItem.Name}] Write Log [{logItem.ItemToSave.Count}]"); string logLine; // 첫번째 write : header 생성 //if (logItem.GetFileSizeInByte() <= 0) { if (!logItem.IsHeaderWritten) { Log.FuncI($"[{logItem.Name}] Insert Header"); logLine = logItem.InsertHeader(logItem.ItemToSave.FirstOrDefault()); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_HEADER_ADDED, (logItem.Name, logItem.CurLogFileName, logLine))); } while (logItem.ItemToSave.Count() > 0) { var logObj = logItem.ItemToSave.Dequeue(); if (logObj is ValueTuple tuple) { if (tuple.Item2) { logLine = logItem.InsertHeader(tuple); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_HEADER_ADDED, (logItem.Name, logItem.CurLogFileName, logLine))); } logLine = logItem.WriteLog(tuple.Item3, tuple.Item1); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_LOG_ADDED, (logItem.Name, logItem.CurLogFileName, logLine))); } else { logLine = logItem.WriteLog(logObj, DateTime.Now); context.EventPublisher.Publish(new EventMessage("CsvLogSeq", EVENT_NOTIFY_LOG_ADDED, (logItem.Name, logItem.CurLogFileName, logLine))); } } //Log.V($"Log[{logItem.Name}] Size (MB) = {logItem.GetFileSizeInMB()}"); //Log.V($"Log[{logItem.Name}] Size (KB) = {logItem.GetFileSizeInByte() / 1024}"); //Log.V($"Log[{logItem.Name}] Size (Byte) = {logItem.GetFileSizeInByte()}"); } } return SequenceAction.Ok(); } /// /// Log Write할 데이터 object 넣기 /// Log Processing 코드(SeqCheckNewLogData)와 conflict 발생하지 않도록 async 주의 /// /// 저장할 CsvLogSetting 로그 이름 (CsvLogIds.xxx) /// 저장할 데이터 객체 (주의: 헤더와 불일치하지 않게, 같은 Class Type의 Object만 넣어주세요. 그리고 csv를 만들기 위해 property 1단계만 사용해주세요) public static void EnqueueLog(string logName, object obj, bool writeHeader = false) { if (obj == null) return; lock (_lock) { var logItem = Logs.FirstOrDefault(x => x.Name == logName); if (logItem != null) { Application.Current.Dispatcher.Invoke(delegate { logItem.ItemToSave.Enqueue(new ValueTuple(DateTime.Now, writeHeader, obj)); }); } } } /// /// 해당 로그아이템의 Full File Path 획득 /// /// CsvLogSetting 로그 이름 (CsvLogIds.xxx) /// 로그파일 이름 /// public static string GetFullFilePath(string logName, string fileName) { if (string.IsNullOrEmpty(logName)) return string.Empty; string result = string.Empty; lock (_lock) { var logItem = Logs.FirstOrDefault(x => x.Name == logName); if (logItem != null) { result = Path.Combine(logItem.CurLogDir, fileName); } } return result; } private static void OnForceError(SequenceContext arg1, string arg2) { } private static void OnStopped(SequenceContext obj) { //lock (_lock) { try { foreach (var logItem in Logs) { // 파일스트림 Close logItem.CloseFileStream(); } } catch (Exception ex) { Log.FuncE("Close Exception : " + ex.ToString()); } } } }