2026-01-26 18:56:53 +09:00

637 lines
26 KiB
C#

/*
* 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));
/// <summary>
/// Param 1 : ProcLogId, 2: BeforeFile, 3: NewFile
/// </summary>
public const string EVENT_NOTIFY_LOG_FILE_CHANGED = "EVENT_NOTIFY_LOG_FILE_CHANGED"; // file open 시 알림
/// <summary>
/// Log Event (헤더 추가) Publish
/// </summary>
public const string EVENT_NOTIFY_HEADER_ADDED = "EVENT_NOTIFY_HEADER_ADDED";
/// <summary>
/// Log Event (로그 추가) Publish
/// </summary>
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<string> 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<ValueTuple<DateTime, bool, object>> ItemToSave = new Queue<ValueTuple<DateTime, bool, object>>();
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;
}
/// <summary>
/// 헤더 추가
/// </summary>
/// <param name="obj">DateTime 포함된 ValueTuple</param>
public string InsertHeader(ValueTuple<DateTime, bool, object> 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;
}
/// <summary>
/// 로그 내용 한 줄 Write
/// </summary>
/// <param name="obj">LogItem (DateTime 포함된 ValueTuple 아님)</param>
/// <param name="dateTime">로그 Timestamp</param>
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<CsvLogItemEx> Logs = new List<CsvLogItemEx>();
private static List<string> OpenFileList = new List<string>();
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();
}
/// <summary>
/// CEqpSeq::InterlockProcess() 포팅
/// </summary>
/// <param name="context"></param>
/// <param name="id"></param>
/// <param name="param"></param>
/// <returns></returns>
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<DateTime, bool, object> 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();
}
/// <summary>
/// Log Write할 데이터 object 넣기
/// Log Processing 코드(SeqCheckNewLogData)와 conflict 발생하지 않도록 async 주의
/// </summary>
/// <param name="logName">저장할 CsvLogSetting 로그 이름 (CsvLogIds.xxx)</param>
/// <param name="obj">저장할 데이터 객체 (주의: 헤더와 불일치하지 않게, 같은 Class Type의 Object만 넣어주세요. 그리고 csv를 만들기 위해 property 1단계만 사용해주세요)</param>
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, bool, object>(DateTime.Now, writeHeader, obj));
});
}
}
}
/// <summary>
/// 해당 로그아이템의 Full File Path 획득
/// </summary>
/// <param name="logName">CsvLogSetting 로그 이름 (CsvLogIds.xxx)</param>
/// <param name="fileName">로그파일 이름</param>
/// <returns></returns>
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());
}
}
}
}