first commit

This commit is contained in:
Stefano Rossi 2025-07-12 18:59:18 +02:00
commit aa44d3ad70
Signed by: chadmin
GPG key ID: 9EFA2130646BC893
1585 changed files with 277994 additions and 0 deletions

View file

@ -0,0 +1,60 @@
using System.Collections.Generic;
using UnityEngine;
#nullable enable
//namespace BgTools.PlayerPrefsEditor
namespace Meryel.UnityCodeAssist.Editor.Preferences
{
[System.Serializable]
public class PreferenceEntryHolder : ScriptableObject
{
public List<PreferenceEntry>? userDefList;
public List<PreferenceEntry>? unityDefList;
private void OnEnable()
{
hideFlags = HideFlags.DontSave;
userDefList ??= new List<PreferenceEntry>();
unityDefList ??= new List<PreferenceEntry>();
}
public void ClearLists()
{
userDefList?.Clear();
unityDefList?.Clear();
}
}
[System.Serializable]
public class PreferenceEntry
{
public enum PrefTypes
{
String = 0,
Int = 1,
Float = 2
}
public PrefTypes m_typeSelection;
public string? m_key;
// Need diffrend ones for auto type selection of serilizedProerty
public string? m_strValue;
public int m_intValue;
public float m_floatValue;
public string? ValueAsString()
{
return m_typeSelection switch
{
PrefTypes.String => m_strValue,
PrefTypes.Int => m_intValue.ToString(),
PrefTypes.Float => m_floatValue.ToString(),
_ => string.Empty,
};
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 38f9da72011fd244b94cc233de93ac92
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,370 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using UnityEditor;
using UnityEngine;
using UnityEditorInternal;
#nullable enable
namespace Meryel.UnityCodeAssist.Editor.Preferences
{
public class PreferenceMonitor
{
private static readonly Lazy<PreferenceMonitor> _instanceOfPlayerPrefs = new Lazy<PreferenceMonitor>(() => new PreferenceMonitor(true));
private static readonly Lazy<PreferenceMonitor> _instanceOfEditorPrefs = new Lazy<PreferenceMonitor>(() => new PreferenceMonitor(false));
public static PreferenceMonitor InstanceOfPlayerPrefs => _instanceOfPlayerPrefs.Value;
public static PreferenceMonitor InstanceOfEditorPrefs => _instanceOfEditorPrefs.Value;
//const int Limit = 128;
const int Limit = 8192;
/// <summary>
/// PlayerPrefs or EditorPrefs
/// </summary>
readonly bool isPlayerPrefs;
#region ErrorValues
private readonly int ERROR_VALUE_INT = int.MinValue;
private readonly string ERROR_VALUE_STR = "<UCA_ERR_2407201713>";
#endregion //ErrorValues
#pragma warning disable CS0414
private static string pathToPrefs = String.Empty;
private static string platformPathPrefix = @"~";
#pragma warning restore CS0414
//private string[] userDef;
//private string[] unityDef;
//private bool showSystemGroup = false;
private SerializedObject? serializedObject;
private ReorderableList? userDefList;
private ReorderableList? unityDefList;
private PreferenceEntryHolder? prefEntryHolder;
private PreferanceStorageAccessor? entryAccessor;
private bool updateView = false;
//private bool monitoring = false;
//private bool showLoadingIndicatorOverlay = false;
#if UNITY_EDITOR_LINUX
private readonly char[] invalidFilenameChars = { '"', '\\', '*', '/', ':', '<', '>', '?', '|' };
#elif UNITY_EDITOR_OSX
private readonly char[] invalidFilenameChars = { '$', '%', '&', '\\', '/', ':', '<', '>', '|', '~' };
#endif
PreferenceMonitor(bool isPlayerPrefs)
{
this.isPlayerPrefs = isPlayerPrefs;
OnEnable();
EditorApplication.update += Update;
}
~PreferenceMonitor()
{
OnDisable();
}
public void Bump()
{
Serilog.Log.Debug("Bumping preference {IsPlayerPrefs}", isPlayerPrefs);
RetrieveAndSendKeysAndValues(false);
}
private void RetrieveAndSendKeysAndValues(bool reloadKeys)
{
string[]? keys = GetKeys(reloadKeys);
if (keys == null)
return;
string[] values = GetKeyValues(reloadKeys, keys, out var stringKeys, out var integerKeys, out var floatKeys, out var booleanKeys);
if (isPlayerPrefs)
NetMQInitializer.Publisher?.SendPlayerPrefs(keys, values, stringKeys, integerKeys, floatKeys);
else
NetMQInitializer.Publisher?.SendEditorPrefs(keys, values, stringKeys, integerKeys, floatKeys, booleanKeys);
}
private void OnEnable()
{
#if UNITY_EDITOR_WIN
if (isPlayerPrefs)
pathToPrefs = @"SOFTWARE\Unity\UnityEditor\" + PlayerSettings.companyName + @"\" + PlayerSettings.productName;
else
pathToPrefs = @"Software\Unity Technologies\Unity Editor 5.x";
platformPathPrefix = @"<CurrentUser>";
entryAccessor = new WindowsPrefStorage(pathToPrefs);
#elif UNITY_EDITOR_OSX
if (isPlayerPrefs)
pathToPrefs = @"Library/Preferences/com." + MakeValidFileName(PlayerSettings.companyName) + "." + MakeValidFileName(PlayerSettings.productName) + ".plist";
else
pathToPrefs = @"Library/Preferences/com.unity3d.UnityEditor5.x.plist";
platformPathPrefix = @"~";
entryAccessor = new MacPrefStorage(pathToPrefs);
//entryAccessor.StartLoadingDelegate = () => { showLoadingIndicatorOverlay = true; };
//entryAccessor.StopLoadingDelegate = () => { showLoadingIndicatorOverlay = false; };
#elif UNITY_EDITOR_LINUX
if (isPlayerPrefs)
pathToPrefs = @".config/unity3d/" + MakeValidFileName(PlayerSettings.companyName) + "/" + MakeValidFileName(PlayerSettings.productName) + "/prefs";
else
pathToPrefs = @".local/share/unity3d/prefs";
platformPathPrefix = @"~";
entryAccessor = new LinuxPrefStorage(pathToPrefs);
#else
Serilog.Log.Warning("Undefined Unity Editor platform");
pathToPrefs = String.Empty;
platformPathPrefix = @"~";
entryAccessor = null;
#endif
if (entryAccessor != null)
{
entryAccessor.PrefEntryChangedDelegate = () => { updateView = true; };
entryAccessor.StartMonitoring();
}
}
// Handel view updates for monitored changes
// Necessary to avoid main thread access issue
private void Update()
{
if (updateView)
{
updateView = false;
//PrepareData();
//Repaint();
Serilog.Log.Debug("Updating preference {IsPlayerPrefs}", isPlayerPrefs);
RetrieveAndSendKeysAndValues(true);
}
}
private void OnDisable()
{
entryAccessor?.StopMonitoring();
}
private void InitReorderedList()
{
if (prefEntryHolder == null)
{
var tmp = Resources.FindObjectsOfTypeAll<PreferenceEntryHolder>();
if (tmp.Length > 0)
{
prefEntryHolder = tmp[0];
}
else
{
prefEntryHolder = ScriptableObject.CreateInstance<PreferenceEntryHolder>();
}
}
serializedObject ??= new SerializedObject(prefEntryHolder);
userDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("userDefList"), false, true, true, true);
unityDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("unityDefList"), false, true, false, false);
}
private string[]? GetKeys(bool reloadKeys)
{
if (entryAccessor == null)
{
Serilog.Log.Warning($"{nameof(entryAccessor)} is null");
return null;
}
string[] keys = entryAccessor.GetKeys(reloadKeys);
if (keys.Length > Limit)
keys = keys.Where(k => !k.StartsWith("unity.") && !k.StartsWith("UnityGraphicsQuality")).Take(Limit).ToArray();
return keys;
}
string[]? _cachedKeyValues = null;
string[]? _cachedStringKeys = null;
string[]? _cachedIntegerKeys = null;
string[]? _cachedFloatKeys = null;
string[]? _cachedBooleanKeys = null;
private string[] GetKeyValues(bool reloadData, string[] keys,
out string[] stringKeys, out string[] integerKeys, out string[] floatKeys, out string[] booleanKeys)
{
if (!reloadData && _cachedKeyValues != null && _cachedKeyValues.Length == keys.Length)
{
stringKeys = _cachedStringKeys!;
integerKeys = _cachedIntegerKeys!;
floatKeys = _cachedFloatKeys!;
booleanKeys = _cachedBooleanKeys!;
return _cachedKeyValues;
}
string[] values = new string[keys.Length];
var stringKeyList = new List<string>();
var integerKeyList = new List<string>();
var floatKeyList = new List<string>();
var boolenKeyList = new List<string>();
for (int i = 0; i < keys.Length; i++)
{
var key = keys[i];
string stringValue;
if (isPlayerPrefs)
stringValue = PlayerPrefs.GetString(key, ERROR_VALUE_STR);
else
stringValue = EditorPrefs.GetString(key, ERROR_VALUE_STR);
if (stringValue != ERROR_VALUE_STR)
{
values[i] = stringValue;
stringKeyList.Add(key);
continue;
}
float floatValue;
if (isPlayerPrefs)
floatValue = PlayerPrefs.GetFloat(key, float.NaN);
else
floatValue = EditorPrefs.GetFloat(key, float.NaN);
if (!float.IsNaN(floatValue))
{
values[i] = floatValue.ToString();
floatKeyList.Add(key);
continue;
}
int intValue;
if (isPlayerPrefs)
intValue = PlayerPrefs.GetInt(key, ERROR_VALUE_INT);
else
intValue = EditorPrefs.GetInt(key, ERROR_VALUE_INT);
if (intValue != ERROR_VALUE_INT)
{
values[i] = intValue.ToString();
integerKeyList.Add(key);
continue;
}
bool boolValue = false;
if (!isPlayerPrefs)
{
bool boolValueTrue = EditorPrefs.GetBool(key, true);
bool boolValueFalse = EditorPrefs.GetBool(key, false);
boolValue = boolValueFalse;
if (boolValueTrue == boolValueFalse)
{
values[i] = boolValueTrue.ToString();
boolenKeyList.Add(key);
continue;
}
}
values[i] = string.Empty;
if (isPlayerPrefs)
{
// Keys with ? causing problems, just ignore them
if (key.Contains("?"))
Serilog.Log.Debug("Invalid {PreferenceType} KEY WITH '?', '{Key}' at {Location}, str:{StringValue}, int:{IntegerValue}, float:{FloatValue}, bool:{BooleanValue}",
(isPlayerPrefs ? "PlayerPrefs" : "EditorPrefs"), key, nameof(GetKeyValues),
stringValue, intValue, floatValue, boolValue);
else
// EditorPrefs gives error for some keys
Serilog.Log.Error("Invalid {PreferenceType} '{Key}' at {Location}, str:{StringValue}, int:{IntegerValue}, float:{FloatValue}, bool:{BooleanValue}",
(isPlayerPrefs ? "PlayerPrefs" : "EditorPrefs"), key, nameof(GetKeyValues),
stringValue, intValue, floatValue, boolValue);
}
}
stringKeys = stringKeyList.ToArray();
integerKeys = integerKeyList.ToArray();
floatKeys = floatKeyList.ToArray();
booleanKeys = boolenKeyList.ToArray();
_cachedKeyValues = values;
_cachedStringKeys = stringKeys;
_cachedIntegerKeys = integerKeys;
_cachedFloatKeys = floatKeys;
_cachedBooleanKeys = booleanKeys;
return values;
}
private void LoadKeys(out string[]? userDef, out string[]? unityDef, bool reloadKeys)
{
if(entryAccessor == null)
{
userDef = null;
unityDef = null;
return;
}
string[] keys = entryAccessor.GetKeys(reloadKeys);
//keys.ToList().ForEach( e => { Debug.Log(e); } );
// Seperate keys int unity defined and user defined
Dictionary<bool, List<string>> groups = keys
.GroupBy((key) => key.StartsWith("unity.") || key.StartsWith("UnityGraphicsQuality"))
.ToDictionary((g) => g.Key, (g) => g.ToList());
unityDef = (groups.ContainsKey(true)) ? groups[true].ToArray() : new string[0];
userDef = (groups.ContainsKey(false)) ? groups[false].ToArray() : new string[0];
}
#if (UNITY_EDITOR_LINUX || UNITY_EDITOR_OSX)
private string MakeValidFileName(string unsafeFileName)
{
string normalizedFileName = unsafeFileName.Trim().Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new StringBuilder();
// We need to use a TextElementEmumerator in order to support UTF16 characters that may take up more than one char(case 1169358)
TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(normalizedFileName);
while (charEnum.MoveNext())
{
string c = charEnum.GetTextElement();
if (c.Length == 1 && invalidFilenameChars.Contains(c[0]))
{
stringBuilder.Append('_');
continue;
}
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c, 0);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
stringBuilder.Append(c);
}
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
#endif
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7885cbf1aab77214c8bbcf789440022d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,287 @@
using System;
using System.Linq;
#if UNITY_EDITOR_WIN
using Microsoft.Win32;
using System.Text;
#elif UNITY_EDITOR_OSX
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
#elif UNITY_EDITOR_LINUX
using System.IO;
using System.Xml;
using System.Xml.Linq;
#endif
#nullable enable
//namespace BgTools.PlayerPrefsEditor
namespace Meryel.UnityCodeAssist.Editor.Preferences
{
public abstract class PreferanceStorageAccessor
{
protected string prefPath;
protected string[] cachedData = new string[0];
protected abstract void FetchKeysFromSystem();
protected PreferanceStorageAccessor(string pathToPrefs)
{
prefPath = pathToPrefs;
}
public string[] GetKeys(bool reloadData = true)
{
if (reloadData || cachedData.Length == 0)
{
FetchKeysFromSystem();
}
return cachedData;
}
public Action? PrefEntryChangedDelegate;
protected bool ignoreNextChange = false;
public void IgnoreNextChange()
{
ignoreNextChange = true;
}
protected virtual void OnPrefEntryChanged()
{
if (ignoreNextChange)
{
ignoreNextChange = false;
return;
}
PrefEntryChangedDelegate?.Invoke();
}
public Action? StartLoadingDelegate;
public Action? StopLoadingDelegate;
public abstract void StartMonitoring();
public abstract void StopMonitoring();
public abstract bool IsMonitoring();
}
#if UNITY_EDITOR_WIN
public class WindowsPrefStorage : PreferanceStorageAccessor
{
readonly RegistryMonitor monitor;
public WindowsPrefStorage(string pathToPrefs) : base(pathToPrefs)
{
monitor = new RegistryMonitor(RegistryHive.CurrentUser, prefPath);
monitor.RegChanged += new EventHandler(OnRegChanged);
}
private void OnRegChanged(object sender, EventArgs e)
{
OnPrefEntryChanged();
}
protected override void FetchKeysFromSystem()
{
cachedData = new string[0];
using (RegistryKey rootKey = Registry.CurrentUser.OpenSubKey(prefPath))
{
if (rootKey != null)
{
cachedData = rootKey.GetValueNames();
rootKey.Close();
}
}
// Clean <key>_h3320113488 nameing
//cachedData = cachedData.Select((key) => { return key.Substring(0, key.LastIndexOf("_h", StringComparison.Ordinal)); }).ToArray();
for (int i = 0; i < cachedData.Length; i++)
{
var indexOfSuffix = cachedData[i].LastIndexOf("_h", StringComparison.Ordinal);
if (indexOfSuffix >= 0)
cachedData[i] = cachedData[i].Substring(0, indexOfSuffix);
}
EncodeAnsiInPlace();
}
public override void StartMonitoring()
{
monitor.Start();
}
public override void StopMonitoring()
{
monitor.Stop();
}
public override bool IsMonitoring()
{
return monitor.IsMonitoring;
}
private void EncodeAnsiInPlace()
{
Encoding utf8 = Encoding.UTF8;
Encoding ansi = Encoding.GetEncoding(1252);
for (int i = 0; i < cachedData.Length; i++)
{
cachedData[i] = utf8.GetString(ansi.GetBytes(cachedData[i]));
}
}
}
#elif UNITY_EDITOR_LINUX
public class LinuxPrefStorage : PreferanceStorageAccessor
{
readonly FileSystemWatcher fileWatcher;
public LinuxPrefStorage(string pathToPrefs) : base(Path.Combine(Environment.GetEnvironmentVariable("HOME"), pathToPrefs))
{
fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(prefPath),
NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite,
Filter = "prefs"
};
fileWatcher.Changed += OnWatchedFileChanged;
}
protected override void FetchKeysFromSystem()
{
cachedData = new string[0];
if (File.Exists(prefPath))
{
XmlReaderSettings settings = new XmlReaderSettings();
XmlReader reader = XmlReader.Create(prefPath, settings);
XDocument doc = XDocument.Load(reader);
cachedData = doc.Element("unity_prefs").Elements().Select((e) => e.Attribute("name").Value).ToArray();
}
}
public override void StartMonitoring()
{
fileWatcher.EnableRaisingEvents = true;
}
public override void StopMonitoring()
{
fileWatcher.EnableRaisingEvents = false;
}
public override bool IsMonitoring()
{
return fileWatcher.EnableRaisingEvents;
}
private void OnWatchedFileChanged(object source, FileSystemEventArgs e)
{
OnPrefEntryChanged();
}
}
#elif UNITY_EDITOR_OSX
public class MacPrefStorage : PreferanceStorageAccessor
{
private readonly FileSystemWatcher fileWatcher;
private readonly DirectoryInfo prefsDirInfo;
private readonly String prefsFileNameWithoutExtension;
public MacPrefStorage(string pathToPrefs) : base(Path.Combine(Environment.GetEnvironmentVariable("HOME"), pathToPrefs))
{
prefsDirInfo = new DirectoryInfo(Path.GetDirectoryName(prefPath));
prefsFileNameWithoutExtension = Path.GetFileNameWithoutExtension(prefPath);
fileWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(prefPath),
NotifyFilter = NotifyFilters.LastWrite,
Filter = Path.GetFileName(prefPath)
};
// MAC delete the old and create a new file instead of updating
fileWatcher.Created += OnWatchedFileChanged;
}
protected override void FetchKeysFromSystem()
{
// Workaround to avoid incomplete tmp phase from MAC OS
foreach (FileInfo info in prefsDirInfo.GetFiles())
{
// Check if tmp PlayerPrefs file exist
if (info.FullName.Contains(prefsFileNameWithoutExtension) && !info.FullName.EndsWith(".plist"))
{
StartLoadingDelegate?.Invoke();
return;
}
}
StopLoadingDelegate?.Invoke();
cachedData = new string[0];
if (File.Exists(prefPath))
{
string fixedPrefsPath = prefPath.Replace("\"", "\\\"").Replace("'", "\\'").Replace("`", "\\`");
var cmdStr = string.Format(@"-p '{0}'", fixedPrefsPath);
string stdOut = String.Empty;
string errOut = String.Empty;
var process = new System.Diagnostics.Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = "plutil";
process.StartInfo.Arguments = cmdStr;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.OutputDataReceived += new DataReceivedEventHandler((sender, evt) => { stdOut += evt.Data + "\n"; });
process.ErrorDataReceived += new DataReceivedEventHandler((sender, evt) => { errOut += evt.Data + "\n"; });
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
MatchCollection matches = Regex.Matches(stdOut, @"(?: "")(.*)(?:"" =>.*)");
cachedData = matches.Cast<Match>().Select((e) => e.Groups[1].Value).ToArray();
}
}
public override void StartMonitoring()
{
fileWatcher.EnableRaisingEvents = true;
}
public override void StopMonitoring()
{
fileWatcher.EnableRaisingEvents = false;
}
public override bool IsMonitoring()
{
return fileWatcher.EnableRaisingEvents;
}
private void OnWatchedFileChanged(object source, FileSystemEventArgs e)
{
OnPrefEntryChanged();
}
}
#endif
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a10351133ed64a488d6be3043f5402a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,339 @@

/*
* Thanks to gr0ss for the inspiration.
*
* https://github.com/gr0ss/RegistryMonitor
*
* 11/08/2019
*/
using System;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using Microsoft.Win32;
//namespace BgTools.PlayerPrefsEditor
namespace Meryel.UnityCodeAssist.Editor.Preferences
{
public class RegistryMonitor : IDisposable
{
#region P/Invoke
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern int RegOpenKeyEx(IntPtr hKey, string subKey, uint options, int samDesired, out IntPtr phkResult);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegNotifyChangeKeyValue(IntPtr hKey, bool bWatchSubtree, RegChangeNotifyFilter dwNotifyFilter, IntPtr hEvent, bool fAsynchronous);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegCloseKey(IntPtr hKey);
private const int KEY_QUERY_VALUE = 0x0001;
private const int KEY_NOTIFY = 0x0010;
private const int STANDARD_RIGHTS_READ = 0x00020000;
private static readonly IntPtr HKEY_CLASSES_ROOT = new IntPtr(unchecked((int)0x80000000));
private static readonly IntPtr HKEY_CURRENT_USER = new IntPtr(unchecked((int)0x80000001));
private static readonly IntPtr HKEY_LOCAL_MACHINE = new IntPtr(unchecked((int)0x80000002));
private static readonly IntPtr HKEY_USERS = new IntPtr(unchecked((int)0x80000003));
private static readonly IntPtr HKEY_PERFORMANCE_DATA = new IntPtr(unchecked((int)0x80000004));
private static readonly IntPtr HKEY_CURRENT_CONFIG = new IntPtr(unchecked((int)0x80000005));
private static readonly IntPtr HKEY_DYN_DATA = new IntPtr(unchecked((int)0x80000006));
#endregion
#region Event handling
/// <summary>
/// Occurs when the specified registry key has changed.
/// </summary>
public event EventHandler RegChanged;
/// <summary>
/// Raises the <see cref="RegChanged"/> event.
/// </summary>
/// <remarks>
/// <p>
/// <b>OnRegChanged</b> is called when the specified registry key has changed.
/// </p>
/// <note type="inheritinfo">
/// When overriding <see cref="OnRegChanged"/> in a derived class, be sure to call
/// the base class's <see cref="OnRegChanged"/> method.
/// </note>
/// </remarks>
protected virtual void OnRegChanged()
{
RegChanged?.Invoke(this, null);
}
/// <summary>
/// Occurs when the access to the registry fails.
/// </summary>
public event ErrorEventHandler Error;
/// <summary>
/// Raises the <see cref="Error"/> event.
/// </summary>
/// <param name="e">The <see cref="Exception"/> which occured while watching the registry.</param>
/// <remarks>
/// <p>
/// <b>OnError</b> is called when an exception occurs while watching the registry.
/// </p>
/// <note type="inheritinfo">
/// When overriding <see cref="OnError"/> in a derived class, be sure to call
/// the base class's <see cref="OnError"/> method.
/// </note>
/// </remarks>
protected virtual void OnError(Exception e)
{
Error?.Invoke(this, new ErrorEventArgs(e));
}
#endregion
#region Private member variables
private IntPtr _registryHive;
private string _registrySubName;
private readonly object _threadLock = new object();
private Thread _thread;
private bool _disposed = false;
private readonly ManualResetEvent _eventTerminate = new ManualResetEvent(false);
private RegChangeNotifyFilter _regFilter = RegChangeNotifyFilter.Key | RegChangeNotifyFilter.Attribute | RegChangeNotifyFilter.Value | RegChangeNotifyFilter.Security;
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
/// </summary>
/// <param name="registryKey">The registry key to monitor.</param>
public RegistryMonitor(RegistryKey registryKey)
{
InitRegistryKey(registryKey.Name);
}
/// <summary>
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
/// </summary>
/// <param name="name">The name.</param>
public RegistryMonitor(string name)
{
if (name == null || name.Length == 0)
throw new ArgumentNullException("name");
InitRegistryKey(name);
}
/// <summary>
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
/// </summary>
/// <param name="registryHive">The registry hive.</param>
/// <param name="subKey">The sub key.</param>
public RegistryMonitor(RegistryHive registryHive, string subKey)
{
InitRegistryKey(registryHive, subKey);
}
/// <summary>
/// Disposes this object.
/// </summary>
public void Dispose()
{
Stop();
_disposed = true;
GC.SuppressFinalize(this);
}
/// <summary>
/// Gets or sets the <see cref="RegChangeNotifyFilter">RegChangeNotifyFilter</see>.
/// </summary>
public RegChangeNotifyFilter RegChangeNotifyFilter
{
get { return _regFilter; }
set
{
lock (_threadLock)
{
if (IsMonitoring)
throw new InvalidOperationException("Monitoring thread is already running");
_regFilter = value;
}
}
}
#region Initialization
private void InitRegistryKey(RegistryHive hive, string name)
{
_registryHive = hive switch
{
RegistryHive.ClassesRoot => HKEY_CLASSES_ROOT,
RegistryHive.CurrentConfig => HKEY_CURRENT_CONFIG,
RegistryHive.CurrentUser => HKEY_CURRENT_USER,
RegistryHive.DynData => HKEY_DYN_DATA,
RegistryHive.LocalMachine => HKEY_LOCAL_MACHINE,
RegistryHive.PerformanceData => HKEY_PERFORMANCE_DATA,
RegistryHive.Users => HKEY_USERS,
_ => throw new InvalidEnumArgumentException("hive", (int)hive, typeof(RegistryHive)),
};
_registrySubName = name;
}
private void InitRegistryKey(string name)
{
string[] nameParts = name.Split('\\');
switch (nameParts[0])
{
case "HKEY_CLASSES_ROOT":
case "HKCR":
_registryHive = HKEY_CLASSES_ROOT;
break;
case "HKEY_CURRENT_USER":
case "HKCU":
_registryHive = HKEY_CURRENT_USER;
break;
case "HKEY_LOCAL_MACHINE":
case "HKLM":
_registryHive = HKEY_LOCAL_MACHINE;
break;
case "HKEY_USERS":
_registryHive = HKEY_USERS;
break;
case "HKEY_CURRENT_CONFIG":
_registryHive = HKEY_CURRENT_CONFIG;
break;
default:
_registryHive = IntPtr.Zero;
throw new ArgumentException("The registry hive '" + nameParts[0] + "' is not supported", "value");
}
_registrySubName = String.Join("\\", nameParts, 1, nameParts.Length - 1);
}
#endregion
/// <summary>
/// <b>true</b> if this <see cref="RegistryMonitor"/> object is currently monitoring;
/// otherwise, <b>false</b>.
/// </summary>
public bool IsMonitoring
{
get { return _thread != null; }
}
/// <summary>
/// Start monitoring.
/// </summary>
public void Start()
{
if (_disposed)
throw new ObjectDisposedException(null, "This instance is already disposed");
lock (_threadLock)
{
if (!IsMonitoring)
{
_eventTerminate.Reset();
_thread = new Thread(new ThreadStart(MonitorThread)) { IsBackground = true };
_thread.Start();
}
}
}
/// <summary>
/// Stops the monitoring thread.
/// </summary>
public void Stop()
{
if (_disposed)
throw new ObjectDisposedException(null, "This instance is already disposed");
lock (_threadLock)
{
Thread thread = _thread;
if (thread != null)
{
_eventTerminate.Set();
thread.Join();
}
}
}
private void MonitorThread()
{
try
{
ThreadLoop();
}
catch (Exception e)
{
OnError(e);
}
_thread = null;
}
private void ThreadLoop()
{
int result = RegOpenKeyEx(_registryHive, _registrySubName, 0, STANDARD_RIGHTS_READ | KEY_QUERY_VALUE | KEY_NOTIFY, out IntPtr registryKey);
if (result != 0)
{
throw new Win32Exception(result);
}
try
{
AutoResetEvent _eventNotify = new AutoResetEvent(false);
WaitHandle[] waitHandles = new WaitHandle[] { _eventNotify, _eventTerminate };
while (!_eventTerminate.WaitOne(0, true))
{
result = RegNotifyChangeKeyValue(registryKey, true, _regFilter, _eventNotify.SafeWaitHandle.DangerousGetHandle(), true);
if (result != 0)
{
throw new Win32Exception(result);
}
if (WaitHandle.WaitAny(waitHandles) == 0)
{
OnRegChanged();
}
}
}
finally
{
if (registryKey != IntPtr.Zero)
{
RegCloseKey(registryKey);
}
}
}
}
/// <summary>
/// Filter for notifications reported by <see cref="RegistryMonitor"/>.
/// </summary>
[Flags]
public enum RegChangeNotifyFilter
{
/// <summary>Notify the caller if a subkey is added or deleted.</summary>
Key = 1,
/// <summary>Notify the caller of changes to the attributes of the key,
/// such as the security descriptor information.</summary>
Attribute = 2,
/// <summary>Notify the caller of changes to a value of the key. This can
/// include adding or deleting a value, or changing an existing value.</summary>
Value = 4,
/// <summary>Notify the caller of changes to the security descriptor
/// of the key.</summary>
Security = 8,
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 231ce51d1620ff843ba99ae307c881e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: