C#:
using Newtonsoft.Json;
using System;
using System.IO;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using HarmonyLib;
namespace Oxide.Plugins
{
[Info("HarmonyLogManager", "ZEODE", "1.0.1")]
[Description("Manages Harmony logging and log rotation")]
public class HarmonyLogManager : CovalencePlugin
{
#region Configuration
private ConfigData config;
public class ConfigData
{
[JsonProperty(PropertyName = "Disable Global Verbose Harmony Logging")]
public bool DisableHarmonyLogging { get; set; } = false;
[JsonProperty(PropertyName = "Enable Log Rotation")]
public bool EnableLogRotation { get; set; } = true;
[JsonProperty(PropertyName = "Log Rotation Mode (daily, hourly, size)")]
public string RotationMode { get; set; } = "daily"; // "daily", "hourly", "size"
[JsonProperty(PropertyName = "Daily Mode Options")]
public DailyRotation Daily { get; set; }
[JsonProperty(PropertyName = "Hourly Mode Options")]
public HourlyRotation Hourly { get; set; }
[JsonProperty(PropertyName = "Size Mode Options")]
public SizeRotation Size { get; set; }
[JsonProperty(PropertyName = "Log File Options")]
public LogFileOptions File { get; set; }
public VersionNumber Version { get; set; }
}
public class DailyRotation
{
[JsonProperty(PropertyName = "Daily Log Rotation Time as HH:MM:SS Format")]
public string DailyRotationTime { get; set; } = "23:59:59"; // HH:MM:SS format
}
public class HourlyRotation
{
[JsonProperty(PropertyName = "Log Rotation Every HH Hours")]
public int RotationHours { get; set; } = 24;
}
public class SizeRotation
{
[JsonProperty(PropertyName = "Max Log File Size (MB)")]
public int MaxFileSizeMB { get; set; } = 50;
[JsonProperty(PropertyName = "Log File Size Checking Interval (seconds)")]
public float FileSizeCheckInterval { get; set; } = 300f;
}
public class LogFileOptions
{
[JsonProperty(PropertyName = "Path to Harmony Log File (default = harmony_log.txt)")]
public string HarmonyLogFile { get; set; } = "harmony_log.txt";
[JsonProperty(PropertyName = "Path to Store Old Log Files (default = oxide/logs/Harmony/")]
public string StoredLogPath { get; set; } = "oxide/logs/Harmony/";
[JsonProperty(PropertyName = "Max Stored Log Files (Oldest Deleted First)")]
public int MaxBackupFiles { get; set; } = 30;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<ConfigData>();
if (config == null)
{
LoadDefaultConfig();
}
else if (config.Version < Version)
{
UpdateConfigValues();
}
}
catch (Exception ex)
{
if (ex is JsonSerializationException || ex is NullReferenceException || ex is JsonReaderException)
{
Puts($"Exception Type: {ex.GetType()}");
Puts($"INFO: {ex}");
return;
}
throw;
}
}
protected override void LoadDefaultConfig()
{
Puts("Configuration file missing or corrupt, creating default config file.");
config = GetDefaultConfig();
SaveConfig();
}
private ConfigData GetDefaultConfig()
{
return new ConfigData
{
Daily = new DailyRotation(),
Hourly = new HourlyRotation(),
Size = new SizeRotation(),
File = new LogFileOptions(),
Version = Version
};
}
protected override void SaveConfig()
{
Config.WriteObject(config, true);
}
private void UpdateConfigValues()
{
ConfigData defaultConfig = GetDefaultConfig();
Puts("Config update detected! Updating config file...");
if (config.Version < new VersionNumber(1, 0, 0))
{
config = defaultConfig;
}
config.Version = Version;
SaveConfig();
Puts("Config update complete!");
}
#endregion
#region Fields
private Timer rotationTimer;
private string harmonyLogPath;
#endregion
#region Initialization
void Init()
{
LoadConfig();
}
private void OnServerInitialized()
{
harmonyLogPath = config.File.HarmonyLogFile;
NextTick(() => PrintWarning($"INFO: HarmonyLogManager loading, stand by..."));
// Wait until any other plugins that might alter the Harmony.DEBUG flag to load before altering the flag
timer.Once(5f, () =>
{
ConfigureHarmonyLogging();
if (config.EnableLogRotation)
{
SetupLogRotation();
}
PrintWarning($"INFO: HarmonyLogManager loaded - Logging: {(config.DisableHarmonyLogging ? "Disabled" : "Enabled")}, Rotation: {(config.EnableLogRotation ? "Enabled" : "Disabled")}");
});
}
void Unload()
{
rotationTimer?.Destroy();
Harmony.DEBUG = true;
}
#endregion
#region Harmony Configuration
private void ConfigureHarmonyLogging()
{
if (config.DisableHarmonyLogging)
{
Harmony.DEBUG = false;
PrintWarning("INFO: Harmony verbose logging disabled globally");
}
else
{
Harmony.DEBUG = true;
PrintWarning("INFO: Harmony verbose logging enabled globally");
}
}
#endregion
#region Log Rotation
private void SetupLogRotation()
{
float intervalSeconds = 0;
switch (config.RotationMode.ToLower())
{
case "hourly":
intervalSeconds = config.Hourly.RotationHours * 3600f;
break;
case "size":
intervalSeconds = config.Size.FileSizeCheckInterval;
break;
case "daily":
default:
intervalSeconds = CalculateSecondsUntilDailyRotation();
break;
}
if (intervalSeconds > 0)
{
rotationTimer = timer.Every(intervalSeconds, () => {
CheckAndRotateLog();
// For daily rotation, recalculate next interval
if (config.RotationMode.ToLower() == "daily")
{
rotationTimer?.Destroy();
SetupLogRotation();
}
});
if (config.RotationMode.ToLower() == "daily")
{
PrintWarning($"INFO: Log rotation scheduled at {config.Daily.DailyRotationTime} daily");
}
else
{
PrintWarning($"INFO: Log rotation scheduled every {intervalSeconds} seconds");
}
}
}
private float CalculateSecondsUntilDailyRotation()
{
try
{
var timeParts = config.Daily.DailyRotationTime.Split(':');
int hours = int.Parse(timeParts[0]);
int minutes = timeParts.Length > 1 ? int.Parse(timeParts[1]) : 0;
int seconds = 0;
var now = DateTime.Now;
var targetTime = new DateTime(now.Year, now.Month, now.Day, hours, minutes, seconds);
// If target time has passed today, schedule for tomorrow
if (targetTime <= now)
{
targetTime = targetTime.AddDays(1);
}
return (float)(targetTime - now).TotalSeconds;
}
catch
{
PrintWarning("INFO: Invalid daily rotation time format, using 24 hours");
return 86400f; // Default to 24 hours
}
}
private void CheckAndRotateLog()
{
if (!File.Exists(harmonyLogPath))
return;
bool shouldRotate = false;
string reason = "";
if (config.RotationMode.ToLower() == "size" || config.Size.MaxFileSizeMB > 0)
{
var fileInfo = new FileInfo(harmonyLogPath);
if (fileInfo.Length > config.Size.MaxFileSizeMB * 1024 * 1024)
{
shouldRotate = true;
reason = $"size ({fileInfo.Length / (1024 * 1024)}MB > {config.Size.MaxFileSizeMB}MB)";
}
}
// For time-based rotation, always rotate when called
if (config.RotationMode.ToLower() != "size")
{
shouldRotate = true;
reason = $"time-based ({config.RotationMode})";
}
if (shouldRotate)
{
RotateLogFile(reason);
}
}
private void RotateLogFile(string reason)
{
try
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
string backupPath = Path.Combine(config.File.StoredLogPath, $"harmony_log_{timestamp}.txt");
Directory.CreateDirectory(config.File.StoredLogPath);
File.Move(harmonyLogPath, backupPath);
File.Create(harmonyLogPath).Close();
PrintWarning($"INFO: Rotated harmony log due to {reason} -> {Path.GetFileName(backupPath)}");
CleanupOldBackups();
}
catch (Exception ex)
{
PrintError($"INFO: Failed to rotate log file: {ex.Message}");
}
}
private void CleanupOldBackups()
{
try
{
var logDir = new DirectoryInfo(config.File.StoredLogPath);
var backupFiles = logDir.GetFiles("harmony_log_*.txt");
if (backupFiles.Length > config.File.MaxBackupFiles)
{
Array.Sort(backupFiles, (x, y) => x.CreationTime.CompareTo(y.CreationTime));
int filesToDelete = backupFiles.Length - config.File.MaxBackupFiles;
for (int i = 0; i < filesToDelete; i++)
{
backupFiles[i].Delete();
PrintWarning($"INFO: Deleted old backup: {backupFiles[i].Name}");
}
}
}
catch (Exception ex)
{
PrintError($"INFO: Failed to cleanup old backups: {ex.Message}");
}
}
#endregion
#region Commands
[Command("harmonylog.rotate")]
private void RotateLogCommand(IPlayer player, string command, string[] args)
{
if (!player.IsAdmin && !player.IsServer)
{
player.Reply("INFO: You need to be an admin to use this command");
return;
}
RotateLogFile("manual command");
player.Reply("INFO: Harmony log rotated manually");
}
[Command("harmonylog.status")]
private void StatusCommand(IPlayer player, string command, string[] args)
{
if (!player.IsAdmin && !player.IsServer)
{
player.Reply("INFO: You need to be an admin to use this command");
return;
}
string status = $"Verbose Harmony Logging: {Harmony.DEBUG}\n";
status += $"Log Rotation: {(config.EnableLogRotation ? "Enabled" : "Disabled")}\n";
status += $"Rotation Mode: {config.RotationMode}\n";
status += $"Max File Size: {config.Size.MaxFileSizeMB}MB\n";
status += $"Max Backup Files: {config.File.MaxBackupFiles}\n";
if (File.Exists(harmonyLogPath))
{
var fileInfo = new FileInfo(harmonyLogPath);
status += $"Current Log Size: {fileInfo.Length / (1024 * 1024)}MB\n";
status += $"Last Modified: {fileInfo.LastWriteTime}";
}
else
{
status += "Current Log: Not found";
}
player.Reply(status);
}
[Command("harmonylog.reload")]
private void ReloadCommand(IPlayer player, string command, string[] args)
{
if (!player.IsAdmin && !player.IsServer)
{
player.Reply("INFO: You need to be an admin to use this command");
return;
}
LoadConfig();
ConfigureHarmonyLogging();
rotationTimer?.Destroy();
if (config.EnableLogRotation)
{
SetupLogRotation();
}
player.Reply("INFO: HarmonyLogManager configuration reloaded");
}
#endregion
}
}