@aryacode/dsh-engineering 0.4.22 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +10 -7
  3. package/cordis.patch.yml +1 -1
  4. package/entries/netlogic.js +266 -0
  5. package/entries/optix.js +13 -0
  6. package/package.json +3 -2
  7. package/vendor/ftview/SKILL.md +6 -0
  8. package/vendor/ftview/lib/model.d.ts +8 -3
  9. package/vendor/ftview/lib/model.js +63 -16
  10. package/vendor/l5x/lib/engine/analysis.d.ts +9 -1
  11. package/vendor/l5x/lib/engine/analysis.js +28 -3
  12. package/vendor/l5x/lib/engine/change-set.js +5 -4
  13. package/vendor/l5x/lib/engine/cip-request.d.ts +1 -0
  14. package/vendor/l5x/lib/engine/cip-request.js +71 -26
  15. package/vendor/l5x/lib/engine/direct-polling.js +12 -6
  16. package/vendor/l5x/lib/engine/review.js +12 -1
  17. package/vendor/l5x/lib/engine/studio-sdk.js +24 -6
  18. package/vendor/mwi/code/mw_code.py +4 -1
  19. package/vendor/mwi/legacy/native-variables.js +9 -1
  20. package/vendor/mwi/lib/read/service.d.ts +7 -1
  21. package/vendor/mwi/lib/read/service.js +13 -1
  22. package/vendor/mwi/lib/session/service.js +7 -1
  23. package/vendor/optix/SKILL.md +68 -0
  24. package/vendor/optix/netlogic/SOURCE.json +12 -0
  25. package/vendor/optix/netlogic/index.json +1987 -0
  26. package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
  27. package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
  28. package/vendor/optix/netlogic/pages/alarming.md +489 -0
  29. package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
  30. package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
  31. package/vendor/optix/netlogic/pages/colors.md +122 -0
  32. package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
  33. package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
  34. package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
  35. package/vendor/optix/netlogic/pages/datagrids.md +123 -0
  36. package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
  37. package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
  38. package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
  39. package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
  40. package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
  41. package/vendor/optix/netlogic/pages/element-access.md +1208 -0
  42. package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
  43. package/vendor/optix/netlogic/pages/events.md +340 -0
  44. package/vendor/optix/netlogic/pages/execute-command.md +202 -0
  45. package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
  46. package/vendor/optix/netlogic/pages/good-practices.md +50 -0
  47. package/vendor/optix/netlogic/pages/information-model.md +89 -0
  48. package/vendor/optix/netlogic/pages/log-output.md +22 -0
  49. package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
  50. package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
  51. package/vendor/optix/netlogic/pages/mqtt.md +240 -0
  52. package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
  53. package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
  54. package/vendor/optix/netlogic/pages/opcua.md +325 -0
  55. package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
  56. package/vendor/optix/netlogic/pages/queries.md +1297 -0
  57. package/vendor/optix/netlogic/pages/random-generation.md +90 -0
  58. package/vendor/optix/netlogic/pages/recipes.md +375 -0
  59. package/vendor/optix/netlogic/pages/recipex.md +2383 -0
  60. package/vendor/optix/netlogic/pages/regex.md +15 -0
  61. package/vendor/optix/netlogic/pages/register-observers.md +346 -0
  62. package/vendor/optix/netlogic/pages/reports.md +141 -0
  63. package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
  64. package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
  65. package/vendor/optix/netlogic/pages/serial-port.md +357 -0
  66. package/vendor/optix/netlogic/pages/sessions.md +58 -0
  67. package/vendor/optix/netlogic/pages/system-node.md +153 -0
  68. package/vendor/optix/netlogic/pages/template-library.md +127 -0
  69. package/vendor/optix/netlogic/pages/threading.md +236 -0
  70. package/vendor/optix/netlogic/pages/translations.md +417 -0
  71. package/vendor/optix/netlogic/pages/trends.md +128 -0
  72. package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
  73. package/vendor/optix/netlogic/pages/users-groups.md +416 -0
  74. package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
  75. package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
  76. package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
@@ -0,0 +1,605 @@
1
+ # Debugging tips
2
+
3
+ > [!NOTE]
4
+ > The following tips are meant to help you debug your FactoryTalk Optix projects. They might help you find issues in your code or understand how the system works. Use them wisely!
5
+
6
+ ## Debugging the `Start` method of NetLogics
7
+
8
+ To debug the `Start` method of NetLogics, you can use the following approach:
9
+
10
+ ```csharp
11
+ #region Using directives
12
+ using System.Diagnostics;
13
+ using FTOptix.NetLogic;
14
+ #endregion
15
+
16
+ public class RuntimeNetLogic1 : BaseNetLogic
17
+ {
18
+ public override void Start()
19
+ {
20
+ // Prompt the user to attach a debugger
21
+ Debugger.Launch();
22
+
23
+ // Rest of the Start method code goes here
24
+ // ...
25
+ }
26
+
27
+ public override void Stop()
28
+ {
29
+ // Insert code to be run when the user-defined logic is stopped
30
+ }
31
+ }
32
+
33
+ ```
34
+
35
+ ## Debugging page load time
36
+
37
+ To debug the page load time, the NativeUI and WebUI debug logs can be used, these will provide you detailed information about the page load time, including the time taken to load each component and the time taken to run each script.
38
+
39
+ 1. Using the Save menu, export the application as an x86 binary anywhere on your PC
40
+ 1. Move the exported folder manually to the target device
41
+ 2. Open a command line prompt in the main FTOptixRuntime.exe folder
42
+ 3. Run the application with:
43
+
44
+ ```
45
+ FTOptixRuntime.exe --logfile-size=10000 --log-level=INFO --module-log=urn:FTOptix:NativeUI=DEBUG;urn:FTOptix:WebUI=DEBUG
46
+ ```
47
+
48
+ Now start using the app, switching screens, interacting with grids and so on.
49
+
50
+ When you are satisfied and you passed trough most pages (or the ones that are taking longer to load), get the three log files from the Logs folder so they can be inspected:
51
+
52
+ 1. Open the [Logs Parsing Tool](https://asem-applicationsoftwareengineers.github.io/OptixRuntimeLogsParser/)
53
+ 2. Select and the three log files from the Logs folder one by one
54
+ 3. Click on the "Parse" button
55
+
56
+ > [!NOTE]
57
+ > The logs parsing tool does not store any data anywhere, it only parses the logs and displays the results in a table. The logs are not sent to any server or stored anywhere, they are only used to display the results in the web app. The source code of the tool is available on [GitHub](https://github.com/ASEM-ApplicationSoftwareEngineers/OptixRuntimeLogsParser)
58
+
59
+ The web app will parse the logs and display the results in a table, showing the time taken to load each component and the time taken to run each script.
60
+
61
+ Now, click the tabs to investigate the results:
62
+
63
+ ### UI Timing
64
+
65
+ This tab shows the time taken to load each component of the page, including the time taken to load the page itself, the time taken to load each component, and the time taken to run each script.
66
+
67
+ > [!NOTE]
68
+ > The first page being loaded will likely produce odd results (with some crazy loading time), as the page is being loaded for the first time and the components are being initialized. This is normal and should not be taken into account when analyzing the results.
69
+
70
+ For example:
71
+
72
+
73
+ | Logs Parser Tool output | Description |
74
+ | --- | --- |
75
+ | Page: Screen1 | The name of the page being loaded |
76
+ | Load time: 1467 ms | The total time taken to load the page |
77
+ | Nodes: 904 (31 ms) | The number of project nodes loaded on the page and the time taken to load them |
78
+ | UI Objects: 280 (31 ms) | The number of UI objects loaded on the page and the time taken to load them |
79
+ | DataBinds and Converters: 127 (7 ms) | The number of data bindings and converters loaded on the page and the time taken to load them |
80
+ | UIObjectsModelControllers: 20 (882 ms) | The number of UI object model controllers loaded on the page and the time taken to load them |
81
+ | EventHandlers: 8 (0 ms) | The number of event handlers loaded on the page and the time taken to load them |
82
+ | MethodInvocations: 2 (0 ms) | The number of method invocations loaded on the page and the time taken to load them |
83
+ | NetLogics: 2 (447 ms) | The number of NetLogics loaded on the page and the time taken to load them |
84
+ | Qt create: 172 ms | The time taken to create the Qt objects |
85
+ | Qt model: 2 ms | The time taken to create the Qt model |
86
+ | Qt total: 1398 ms | The total time taken to create the Qt objects and pass them to the graphics card |
87
+
88
+ In this example, the page took 1467 ms to load, with the majority of the time being spent on loading the UI objects model controllers (882 ms) and the NetLogics (447 ms). This means that the page is likely to be slow due to the complexity of the UI objects model controllers and the NetLogics, it might be safe to assume that some portion of the UI are waiting for the C# scripts to be completed for the page to be rendered.
89
+
90
+ ### Warnings
91
+
92
+ Although not directly related to the page load time, this tab shows the warnings that were logged during the page load. This can help you identify potential issues in your code that might be causing the page to load slowly.
93
+
94
+ > [!TIP]
95
+ > If you see any warnings in this tab, it is recommended to investigate them and fix them, as they might be causing the page to load slowly or might cause issues in the future.
96
+
97
+ ### Errors
98
+
99
+ This tab shows the errors that were logged during the page load. This can help you identify potential issues in your code that might be causing the page to fail to load or to load slowly.
100
+
101
+ Before attempting any app optimization, it is recommended to check this tab and fix any errors that might be causing the page to fail to load or to load slowly.
102
+
103
+ ## Getting total count of project nodes
104
+
105
+ - `LogicObject.Context.NodeCount` -> returns the total number of nodes (whole project)
106
+
107
+ ## Reading debug logs
108
+
109
+ When debug logs are enabled, the output of some rendering methods is logged to the output, here is an example:
110
+
111
+ ```txt
112
+ 2024-10-22 10:55:05 NativeUI StartNetLogics,10,6,600.000
113
+ ```
114
+
115
+ The syntax is:
116
+
117
+ ```txt
118
+ [Timestamp] [Module Name] [Method Name];[Total time in ms],[Number of processed items],[Items per second]
119
+ ```
120
+
121
+ So, in the example above we get the NativeUI:
122
+
123
+ - Calling the `Start` method of all NetLogics in the page
124
+ - Starting a total of 6 NetLogics
125
+ - Running at a theoretical 600 NetLogics per second can be loaded (this is something like benchmarking)
126
+
127
+ ## Measure code run time with Stopwatch
128
+
129
+ Using `DateTime.Now` to measure code run time is not accurate, as it is affected by system time changes and has a low resolution. Instead, use the `Stopwatch` class from the `System.Diagnostics` namespace, which provides a high-resolution timer specifically designed for measuring elapsed time.
130
+
131
+ ```csharp
132
+ // Start stopwatch
133
+ Stopwatch stopwatch = new Stopwatch();
134
+ stopwatch.Start();
135
+
136
+ // code to measure
137
+
138
+ stopwatch.Stop();
139
+ Log.Info("Run time (ms): " + stopwatch.Elapsed.TotalMilliseconds);
140
+ ```
141
+
142
+ ## Simple profiler
143
+
144
+ This code can be used to create a simple profiler that logs memory usage, CPU usage, number of variables, tags, sessions, and active alarms every minute to a log file. The log files are rotated when they reach a specified size limit, and a maximum number of log files are kept.
145
+
146
+ This code can easily be extended to log additional metrics as needed.
147
+
148
+ ```csharp
149
+ #region Using directives
150
+ using System;
151
+ using System.Collections.Generic;
152
+ using System.Diagnostics;
153
+ using System.IO;
154
+ using System.Linq;
155
+ using System.Threading.Tasks;
156
+ using FTOptix.CommunicationDriver;
157
+ using FTOptix.Core;
158
+ using FTOptix.CoreBase;
159
+ using FTOptix.HMIProject;
160
+ using FTOptix.NativeUI;
161
+ using FTOptix.NetLogic;
162
+ using FTOptix.Retentivity;
163
+ using FTOptix.UI;
164
+ using UAManagedCore;
165
+ using FTOptix.RAEtherNetIP;
166
+ using OpcUa = UAManagedCore.OpcUa;
167
+ using FTOptix.Alarm;
168
+ #endregion
169
+
170
+ public class SimpleProfiler : BaseNetLogic
171
+ {
172
+ private FileLogger logger;
173
+
174
+ public override void Start()
175
+ {
176
+ // Initialize the logger with a directory path
177
+ // Logs will be stored in the project's ApplicationData folder
178
+ string logDirectory = ResourceUri.FromApplicationRelativePath("").Uri;
179
+
180
+ // Create logger with:
181
+ // - Log directory path
182
+ // - File prefix "SimpleProfiler"
183
+ // - Max 10 MB per file
184
+ // - Keep maximum 5 log files
185
+ logger = new FileLogger(logDirectory, "Profiler", maxFileSizeMB: 10, maxFileCount: 5);
186
+
187
+ // Example usage
188
+ logger.WriteLog("SimpleProfiler started");
189
+
190
+ // Set up a periodic task to log memory usage every minute
191
+ profilerTask = new PeriodicTask(LogMemoryUsage, 60000, LogicObject);
192
+ profilerTask.Start();
193
+ }
194
+
195
+ private void LogMemoryUsage()
196
+ {
197
+ var stringBuilder = new System.Text.StringBuilder();
198
+ try
199
+ {
200
+ var cpuTask = GetMemoryUsageForProcess();
201
+ cpuTask.Wait();
202
+ var memoryUsage = cpuTask.Result;
203
+ stringBuilder.Append($"RAM:{memoryUsage / 1024}KB;");
204
+ }
205
+ catch (Exception ex)
206
+ {
207
+ Log.Debug("ERROR", $"Failed to get memory usage: {ex.Message}");
208
+ }
209
+
210
+ try
211
+ {
212
+ var cpuUsageTask = GetCpuUsageForProcess();
213
+ cpuUsageTask.Wait();
214
+ double cpuUsage = cpuUsageTask.Result;
215
+ stringBuilder.Append($"CPU:{cpuUsage:F2}%;");
216
+ }
217
+ catch (Exception ex)
218
+ {
219
+ Log.Debug("ERROR", $"Failed to get CPU usage: {ex.Message}");
220
+ }
221
+
222
+ try
223
+ {
224
+ long variablesCount = GetInstancesOfTypeRecursive(InformationModel.Get(OpcUa.VariableTypes.BaseVariableType)).Count;
225
+ stringBuilder.Append($"Vars:{variablesCount};");
226
+ }
227
+ catch (Exception ex)
228
+ {
229
+ Log.Debug("ERROR", $"Failed to get variables count: {ex.Message}");
230
+ }
231
+
232
+ try
233
+ {
234
+ long retainedAlarmsCount = GetRetainedAlarmsCount();
235
+ stringBuilder.Append($"ActiveAlarms:{retainedAlarmsCount};");
236
+ }
237
+ catch (Exception ex)
238
+ {
239
+ Log.Debug("ERROR", $"Failed to get retained alarms count: {ex.Message}");
240
+ }
241
+
242
+ try
243
+ {
244
+ long tagsCount = GetInstancesOfTypeRecursive(InformationModel.Get(FTOptix.CommunicationDriver.VariableTypes.Tag)).Count;
245
+ stringBuilder.Append($"Tags:{tagsCount};");
246
+ }
247
+ catch (Exception ex)
248
+ {
249
+ Log.Debug("ERROR", $"Failed to get tags count: {ex.Message}");
250
+ }
251
+
252
+ try
253
+ {
254
+ long sessionsCount = GetInstancesOfTypeRecursive(InformationModel.Get(FTOptix.Core.ObjectTypes.Session)).Count;
255
+ stringBuilder.Append($"Sessions:{sessionsCount};");
256
+ }
257
+ catch (Exception ex)
258
+ {
259
+ Log.Debug("ERROR", $"Failed to get sessions count: {ex.Message}");
260
+ }
261
+
262
+ try
263
+ {
264
+ long nodesCount = LogicObject.Context.NodeCount;
265
+ stringBuilder.Append($"TotalNodes:{nodesCount};");
266
+ }
267
+ catch (Exception ex)
268
+ {
269
+ Log.Debug("ERROR", $"Failed to get total nodes count: {ex.Message}");
270
+ }
271
+
272
+ if (stringBuilder.Length > 0)
273
+ {
274
+ logger.WriteLog(stringBuilder.ToString());
275
+ }
276
+ else
277
+ {
278
+ logger.WriteLog("No profiling data collected.");
279
+ }
280
+ }
281
+
282
+ public override void Stop()
283
+ {
284
+ // Log when stopping
285
+ if (logger != null)
286
+ {
287
+ logger.WriteLog("SimpleProfiler stopped");
288
+ }
289
+
290
+ profilerTask?.Dispose();
291
+ logger = null;
292
+ profilerTask = null;
293
+ }
294
+
295
+ /// <summary>
296
+ /// Gets the total count of retained (active) alarms in the system
297
+ /// </summary>
298
+ /// <returns>The number of retained alarms, or 0 if unable to retrieve the count</returns>
299
+ private long GetRetainedAlarmsCount()
300
+ {
301
+ try
302
+ {
303
+ var retainedAlarmsObject = LogicObject.Context.GetNode(FTOptix.Alarm.Objects.RetainedAlarms);
304
+ if (retainedAlarmsObject != null)
305
+ {
306
+ var localizedAlarmsVariable = retainedAlarmsObject.GetVariable("LocalizedAlarms");
307
+ if (localizedAlarmsVariable != null)
308
+ {
309
+ var localizedAlarmsNodeId = (NodeId)localizedAlarmsVariable.Value;
310
+ if (localizedAlarmsNodeId != null && !localizedAlarmsNodeId.IsEmpty)
311
+ {
312
+ var localizedAlarmsContainer = LogicObject.Context.GetNode(localizedAlarmsNodeId);
313
+ if (localizedAlarmsContainer != null)
314
+ {
315
+ return localizedAlarmsContainer.Children.Count;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ catch (Exception ex)
322
+ {
323
+ Log.Warning("SimpleProfiler", $"Failed to get retained alarms count: {ex.Message}");
324
+ }
325
+
326
+ return 0;
327
+ }
328
+
329
+ /// <summary>
330
+ /// Recursively gets all instances of a type and its subtypes
331
+ /// </summary>
332
+ /// <param name="typeNode">The type node to get instances from</param>
333
+ /// <returns>List of all instances of the type and its subtypes</returns>
334
+ private List<IUANode> GetInstancesOfTypeRecursive(IUANode typeNode)
335
+ {
336
+ // Get all instances that have this type as their TypeDefinition
337
+ var result = typeNode.InverseRefs.GetNodes(OpcUa.ReferenceTypes.HasTypeDefinition, false).ToList();
338
+
339
+ // Get all subtypes of this type
340
+ var subtypes = typeNode.Refs.GetNodes(OpcUa.ReferenceTypes.HasSubtype, false);
341
+
342
+ // Recursively get instances from each subtype
343
+ foreach (var subtype in subtypes)
344
+ {
345
+ var subtypeInstances = GetInstancesOfTypeRecursive(subtype);
346
+ result.AddRange(subtypeInstances);
347
+ }
348
+
349
+ return result;
350
+ }
351
+
352
+ /// <summary>
353
+ /// Asynchronously calculates the CPU usage percentage for the current process over a short interval.
354
+ /// </summary>
355
+ /// <remarks>This method measures the CPU usage of the current process by sampling the total processor
356
+ /// time over a fixed delay period. The result represents the percentage of CPU time used by the process relative to
357
+ /// the total available CPU time across all cores.</remarks>
358
+ /// <returns>A <see cref="double"/> representing the CPU usage percentage of the current process. The value ranges from 0 to
359
+ /// 100, where 100 indicates full utilization of all CPU cores.</returns>
360
+ private async Task<double> GetCpuUsageForProcess()
361
+ {
362
+ var stopWatch = new Stopwatch();
363
+ stopWatch.Start();
364
+ var startCpuUsage = Process.GetCurrentProcess().TotalProcessorTime;
365
+ await Task.Delay(500);
366
+
367
+ stopWatch.Stop();
368
+ var endCpuUsage = Process.GetCurrentProcess().TotalProcessorTime;
369
+ var cpuUsedMs = (endCpuUsage - startCpuUsage).TotalMilliseconds;
370
+ var cpuUsageTotal = cpuUsedMs / (Environment.ProcessorCount * stopWatch.Elapsed.TotalMilliseconds);
371
+ return cpuUsageTotal * 100;
372
+ }
373
+
374
+ private async Task<double> GetMemoryUsageForProcess()
375
+ {
376
+ return await Task.FromResult(Process.GetCurrentProcess().PrivateMemorySize64);
377
+ }
378
+
379
+ private PeriodicTask profilerTask;
380
+ }
381
+
382
+ public class FileLogger
383
+ {
384
+ private readonly string _logDirectory;
385
+ private readonly string _logFilePrefix;
386
+ private readonly int _maxFileSizeMB;
387
+ private readonly int _maxFileCount;
388
+ private readonly object _lockObject = new object();
389
+ private string _currentLogFile;
390
+
391
+ /// <summary>
392
+ /// Initializes a new instance of the FileLogger class
393
+ /// </summary>
394
+ /// <param name="logDirectory">Directory where log files will be stored</param>
395
+ /// <param name="logFilePrefix">Prefix for log file names (default: "log")</param>
396
+ /// <param name="maxFileSizeMB">Maximum size of each log file in MB (default: 10)</param>
397
+ /// <param name="maxFileCount">Maximum number of log files to keep (default: 5)</param>
398
+ public FileLogger(string logDirectory, string logFilePrefix = "log", int maxFileSizeMB = 10, int maxFileCount = 5)
399
+ {
400
+ _logDirectory = logDirectory;
401
+ _logFilePrefix = logFilePrefix;
402
+ _maxFileSizeMB = maxFileSizeMB;
403
+ _maxFileCount = maxFileCount;
404
+
405
+ // Ensure log directory exists
406
+ if (!Directory.Exists(_logDirectory))
407
+ {
408
+ Directory.CreateDirectory(_logDirectory);
409
+ }
410
+
411
+ // Initialize current log file
412
+ _currentLogFile = GetCurrentLogFilePath();
413
+ }
414
+
415
+ /// <summary>
416
+ /// Writes a message to the log file with timestamp
417
+ /// </summary>
418
+ /// <param name="message">The message to log</param>
419
+ public void WriteLog(string message)
420
+ {
421
+ lock (_lockObject)
422
+ {
423
+ try
424
+ {
425
+ // Check if rotation is needed
426
+ CheckAndRotateLogFile();
427
+
428
+ // Write the log entry with timestamp
429
+ string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}";
430
+ File.AppendAllText(_currentLogFile, logEntry + Environment.NewLine);
431
+ }
432
+ catch (Exception ex)
433
+ {
434
+ // Fallback to debug output if file write fails
435
+ Log.Error($"FileLogger - Failed to write log: {ex.Message}");
436
+ }
437
+ }
438
+ }
439
+
440
+ /// <summary>
441
+ /// Writes a message to the log file with a specified log level
442
+ /// </summary>
443
+ /// <param name="level">Log level (INFO, WARNING, ERROR, etc.)</param>
444
+ /// <param name="message">The message to log</param>
445
+ public void WriteLog(string level, string message)
446
+ {
447
+ WriteLog($"[{level}] {message}");
448
+ }
449
+
450
+ /// <summary>
451
+ /// Checks if the current log file exceeds the size limit and rotates if necessary
452
+ /// </summary>
453
+ private void CheckAndRotateLogFile()
454
+ {
455
+ if (File.Exists(_currentLogFile))
456
+ {
457
+ FileInfo fileInfo = new FileInfo(_currentLogFile);
458
+ long maxSizeBytes = _maxFileSizeMB * 1024 * 1024;
459
+
460
+ if (fileInfo.Length >= maxSizeBytes)
461
+ {
462
+ RotateLogFiles();
463
+ }
464
+ }
465
+ }
466
+
467
+ /// <summary>
468
+ /// Rotates log files by renaming them with incremental numbers
469
+ /// </summary>
470
+ private void RotateLogFiles()
471
+ {
472
+ try
473
+ {
474
+ // Get all existing log files with numbers in their names
475
+ var logFiles = Directory.GetFiles(_logDirectory, $"{_logFilePrefix}*.txt")
476
+ .Select(f => new FileInfo(f))
477
+ .ToList();
478
+
479
+ // Parse file numbers and find the highest number
480
+ int highestNumber = 0;
481
+ var numberedFiles = new List<(FileInfo file, int number)>();
482
+
483
+ foreach (var file in logFiles)
484
+ {
485
+ string nameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name);
486
+
487
+ // Check if this is the base file (no number)
488
+ if (nameWithoutExtension == _logFilePrefix)
489
+ {
490
+ numberedFiles.Add((file, 0));
491
+ }
492
+ else if (nameWithoutExtension.StartsWith(_logFilePrefix + "_"))
493
+ {
494
+ string numberPart = nameWithoutExtension.Substring(_logFilePrefix.Length + 1);
495
+ if (int.TryParse(numberPart, out int number))
496
+ {
497
+ numberedFiles.Add((file, number));
498
+ if (number > highestNumber)
499
+ {
500
+ highestNumber = number;
501
+ }
502
+ }
503
+ }
504
+ }
505
+
506
+ // Sort by number (descending) to rename from highest to lowest
507
+ numberedFiles = numberedFiles.OrderByDescending(x => x.number).ToList();
508
+
509
+ // Delete files that exceed the max count
510
+ if (numberedFiles.Count >= _maxFileCount)
511
+ {
512
+ var filesToDelete = numberedFiles.Skip(_maxFileCount - 1).ToList();
513
+ foreach (var (file, _) in filesToDelete)
514
+ {
515
+ try
516
+ {
517
+ file.Delete();
518
+ }
519
+ catch (Exception ex)
520
+ {
521
+ Log.Error($"FileLogger - Failed to delete old log file: {ex.Message}");
522
+ }
523
+ }
524
+ // Remove deleted files from the list
525
+ numberedFiles = numberedFiles.Take(_maxFileCount - 1).ToList();
526
+ }
527
+
528
+ // Rename existing files (shift numbers up)
529
+ foreach (var (file, currentNumber) in numberedFiles)
530
+ {
531
+ if (file.Exists)
532
+ {
533
+ int newNumber = currentNumber + 1;
534
+ string newFileName = Path.Combine(_logDirectory, $"{_logFilePrefix}_{newNumber}.txt");
535
+
536
+ try
537
+ {
538
+ // If target file exists, we need to find the next available number
539
+ while (File.Exists(newFileName))
540
+ {
541
+ newNumber++;
542
+ newFileName = Path.Combine(_logDirectory, $"{_logFilePrefix}_{newNumber}.txt");
543
+ }
544
+
545
+ file.MoveTo(newFileName);
546
+ }
547
+ catch (Exception ex)
548
+ {
549
+ Log.Error($"FileLogger - Failed to rotate log file: {ex.Message}");
550
+ }
551
+ }
552
+ }
553
+
554
+ // Current log file path remains the same (base name without number)
555
+ // This ensures we always append to a new file after rotation
556
+ _currentLogFile = GetCurrentLogFilePath();
557
+ }
558
+ catch (Exception ex)
559
+ {
560
+ Log.Error($"FileLogger - Failed to rotate log files: {ex.Message}");
561
+ }
562
+ }
563
+
564
+ /// <summary>
565
+ /// Gets the path for the current log file
566
+ /// </summary>
567
+ /// <returns>Full path to the current log file</returns>
568
+ private string GetCurrentLogFilePath()
569
+ {
570
+ return Path.Combine(_logDirectory, $"{_logFilePrefix}.txt");
571
+ }
572
+
573
+ /// <summary>
574
+ /// Clears all log files in the log directory
575
+ /// </summary>
576
+ public void ClearAllLogs()
577
+ {
578
+ lock (_lockObject)
579
+ {
580
+ try
581
+ {
582
+ var logFiles = Directory.GetFiles(_logDirectory, $"{_logFilePrefix}*.txt");
583
+ foreach (var file in logFiles)
584
+ {
585
+ try
586
+ {
587
+ File.Delete(file);
588
+ }
589
+ catch (Exception ex)
590
+ {
591
+ Log.Error($"FileLogger - Failed to delete log file {file}: {ex.Message}");
592
+ }
593
+ }
594
+
595
+ // Recreate current log file path (file will be created on first write)
596
+ _currentLogFile = GetCurrentLogFilePath();
597
+ }
598
+ catch (Exception ex)
599
+ {
600
+ Log.Error($"FileLogger - Failed to clear log files: {ex.Message}");
601
+ }
602
+ }
603
+ }
604
+ }
605
+ ```