Skip to content

Commit 8f19b62

Browse files
committed
Implement custom FileSystemWatchers
1 parent 957c324 commit 8f19b62

3 files changed

Lines changed: 110 additions & 178 deletions

File tree

src/OxideMod.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
using System.Reflection;
1919
using System.Runtime.InteropServices;
2020
using System.Threading;
21+
using Oxide.IO;
22+
using Oxide.IO.Unix;
23+
using Oxide.IO.Windows;
2124
using Timer = Oxide.Core.Libraries.Timer;
2225

2326
namespace Oxide.Core
@@ -67,6 +70,10 @@ public sealed class OxideMod
6770
public string LangDirectory { get; private set; }
6871
public string LogDirectory { get; private set; }
6972

73+
public IFileSystem FileSystem { get; private set; }
74+
75+
public IFileSystemWatcher FileWatcher { get; private set; }
76+
7077
// Gets the number of seconds since the server started
7178
public float Now => getTimeSinceStartup();
7279

@@ -143,7 +150,9 @@ public void Load()
143150
CommandLine.GetArgument("oxide.directory", out string var, out string format);
144151
if (string.IsNullOrEmpty(var) || CommandLine.HasVariable(var))
145152
{
146-
InstanceDirectory = Path.Combine(RootDirectory, Utility.CleanPath(string.Format(format, CommandLine.GetVariable(var))));
153+
InstanceDirectory = Path.Combine(RootDirectory,
154+
Utility.CleanPath(string.Format(format,
155+
CommandLine.GetVariable(var))));
147156
}
148157
}
149158

@@ -243,6 +252,21 @@ public void Load()
243252
extensionManager.RegisterLibrary("Timer", libtimer = new Timer());
244253
extensionManager.RegisterLibrary("WebRequests", new WebRequests());
245254

255+
if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX)
256+
{
257+
FileSystem = new UnixFileSystem();
258+
FileWatcher = new UnixFileSystemWatcher(FileSystem, InstanceDirectory, true, NotifyMask.Default);
259+
}
260+
else
261+
{
262+
FileSystem = new WindowsFileSystem();
263+
FileWatcher = new WindowsFileSystemWatcher(FileSystem, InstanceDirectory, true, NotifyMask.Default);
264+
}
265+
266+
FileWatcher.Ignore("*.log")
267+
.Ignore("*.txt")
268+
.BeginInit();
269+
246270
LogInfo("Loading extensions...");
247271
extensionManager.LoadAllExtensions(ExtensionDirectory);
248272

@@ -273,6 +297,8 @@ public void Load()
273297
watcher.OnPluginAdded += watcher_OnPluginAdded;
274298
watcher.OnPluginRemoved += watcher_OnPluginRemoved;
275299
}
300+
301+
FileWatcher.EndInit();
276302
}
277303

278304
/// <summary>
@@ -756,6 +782,7 @@ public void OnShutdown()
756782
RemoteConsole?.Shutdown();
757783
ServerConsole?.OnDisable();
758784
RootLogger.Shutdown();
785+
FileWatcher.Dispose();
759786
}
760787
}
761788

src/Plugins/Watchers/FSWatcher.cs

Lines changed: 78 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,73 @@
11
extern alias References;
22

33
using Oxide.Core.Libraries;
4-
using References::Mono.Posix;
54
using System;
65
using System.Collections.Generic;
76
using System.IO;
8-
#if !NETSTANDARD
9-
using System.Security.Permissions;
10-
#endif
117
using System.Text.RegularExpressions;
8+
using Oxide.IO;
129

1310
namespace Oxide.Core.Plugins.Watchers
1411
{
1512
/// <summary>
1613
/// Represents a file system watcher
1714
/// </summary>
18-
public sealed class FSWatcher : PluginChangeWatcher
15+
public sealed class FSWatcher : PluginChangeWatcher, IObserver<FileSystemEvent>
1916
{
2017
private class QueuedChange
2118
{
2219
internal WatcherChangeTypes type;
2320
internal Timer.TimerInstance timer;
2421
}
2522

26-
// The filesystem watcher
27-
private FileSystemWatcher watcher;
28-
2923
// The plugin list
3024
private ICollection<string> watchedPlugins;
25+
private Timer timers;
26+
private readonly string _directory;
27+
private readonly Regex filter;
28+
private readonly IFileSystemWatcher watcher;
29+
private readonly IDisposable subscription;
3130

32-
// Changes are buffered briefly to avoid duplicate events
33-
private Dictionary<string, QueuedChange> changeQueue;
31+
public FSWatcher(IFileSystemWatcher watcher, string watchedDirectory, string fileFilter = "*")
32+
{
33+
if (!watchedDirectory.StartsWith(watcher.Directory, StringComparison.InvariantCulture))
34+
{
35+
throw new ArgumentException($"Path must be begin with {watcher.Directory}", nameof(watchedDirectory));
36+
}
3437

35-
private Timer timers;
38+
if (string.IsNullOrEmpty(fileFilter))
39+
{
40+
fileFilter = "*";
41+
}
3642

37-
private Dictionary<string, FileSystemWatcher> m_symlinkWatchers = new Dictionary<string, FileSystemWatcher>();
43+
_directory = watchedDirectory;
44+
fileFilter = Regex.Escape(fileFilter)
45+
.Replace("\\*", ".*")
46+
.Replace("\\?", ".");
47+
fileFilter = "^" + fileFilter + "$";
48+
filter = new Regex(fileFilter, RegexOptions.Compiled | RegexOptions.IgnoreCase);
49+
this.watcher = watcher;
3850

39-
/// <summary>
40-
/// Initializes a new instance of the FSWatcher class
41-
/// </summary>
42-
/// <param name="directory"></param>
43-
/// <param name="filter"></param>
44-
public FSWatcher(string directory, string filter)
45-
{
4651
watchedPlugins = new HashSet<string>();
47-
changeQueue = new Dictionary<string, QueuedChange>();
4852
timers = Interface.Oxide.GetLibrary<Timer>();
4953

5054
if (Interface.Oxide.Config.Options.PluginWatchers)
5155
{
52-
LoadWatcher(directory, filter);
53-
54-
// Watch symlinked files
55-
if (Environment.OSVersion.Platform == PlatformID.Unix)
56-
{
57-
foreach (FileInfo fileInfo in new DirectoryInfo(directory).GetFiles(filter))
58-
{
59-
if (IsFileSymlink(fileInfo.FullName))
60-
{
61-
LoadWatcherSymlink(fileInfo.FullName);
62-
}
63-
}
64-
}
56+
subscription = this.watcher.Subscribe(this);
6557
}
6658
else
6759
{
6860
Interface.Oxide.LogWarning("Automatic plugin reloading and unloading has been disabled");
6961
}
7062
}
7163

72-
private bool IsFileSymlink(string path)
73-
{
74-
return (File.GetAttributes(path) & FileAttributes.ReparsePoint) > 0;
75-
}
76-
77-
78-
#if !NETSTANDARD
79-
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
80-
#endif
81-
private void LoadWatcherSymlink(string path)
82-
{
83-
string realPath = Syscall.readlink(path);
84-
string realDirName = Path.GetDirectoryName(realPath);
85-
string realFileName = Path.GetFileName(realPath);
86-
87-
void symlinkTarget_Changed(object sender, FileSystemEventArgs e) => watcher_Changed(sender, e);
88-
89-
FileSystemWatcher watcher = new FileSystemWatcher(realDirName, realFileName);
90-
m_symlinkWatchers[path] = watcher;
91-
watcher.Changed += symlinkTarget_Changed;
92-
watcher.Created += symlinkTarget_Changed;
93-
watcher.Deleted += symlinkTarget_Changed;
94-
watcher.Error += watcher_Error;
95-
watcher.NotifyFilter = NotifyFilters.LastWrite;
96-
watcher.IncludeSubdirectories = false;
97-
watcher.EnableRaisingEvents = true;
98-
}
99-
10064
/// <summary>
101-
/// Loads the filesystem watcher
65+
/// Initializes a new instance of the FSWatcher class
10266
/// </summary>
10367
/// <param name="directory"></param>
10468
/// <param name="filter"></param>
105-
#if !NETSTANDARD
106-
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
107-
#endif
108-
private void LoadWatcher(string directory, string filter)
69+
public FSWatcher(string directory, string filter = "*") : this(Interface.Oxide.FileWatcher, directory, filter)
10970
{
110-
// Create the watcher
111-
watcher = new FileSystemWatcher(directory, filter);
112-
watcher.Changed += watcher_Changed;
113-
watcher.Created += watcher_Changed;
114-
watcher.Deleted += watcher_Changed;
115-
watcher.Error += watcher_Error;
116-
watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
117-
watcher.IncludeSubdirectories = true;
118-
watcher.EnableRaisingEvents = true;
119-
GC.KeepAlive(watcher);
12071
}
12172

12273
/// <summary>
@@ -136,126 +87,77 @@ private void LoadWatcher(string directory, string filter)
13687
/// </summary>
13788
/// <param name="sender"></param>
13889
/// <param name="e"></param>
139-
private void watcher_Changed(object sender, FileSystemEventArgs e)
90+
private void watcher_Changed(object sender, FileSystemEvent e)
14091
{
141-
FileSystemWatcher watcher = (FileSystemWatcher)sender;
142-
int length = e.FullPath.Length - watcher.Path.Length - Path.GetExtension(e.Name).Length - 1;
143-
string subPath = e.FullPath.Substring(watcher.Path.Length + 1, length);
144-
145-
if (!changeQueue.TryGetValue(subPath, out QueuedChange change))
146-
{
147-
change = new QueuedChange();
148-
changeQueue[subPath] = change;
149-
}
150-
change.timer?.Destroy();
151-
change.timer = null;
152-
153-
switch (e.ChangeType)
154-
{
155-
case WatcherChangeTypes.Changed:
156-
if (change.type != WatcherChangeTypes.Created)
157-
{
158-
change.type = WatcherChangeTypes.Changed;
159-
}
160-
break;
161-
162-
case WatcherChangeTypes.Created:
163-
if (change.type == WatcherChangeTypes.Deleted)
164-
{
165-
change.type = WatcherChangeTypes.Changed;
166-
}
167-
else
168-
{
169-
change.type = WatcherChangeTypes.Created;
170-
}
171-
break;
172-
173-
case WatcherChangeTypes.Deleted:
174-
if (change.type == WatcherChangeTypes.Created)
175-
{
176-
changeQueue.Remove(subPath);
177-
return;
178-
}
179-
180-
change.type = WatcherChangeTypes.Deleted;
181-
break;
182-
}
92+
string fullPath = Path.Combine(e.Directory, e.Name);
93+
int length = fullPath.Length - _directory.Length - Path.GetExtension(e.Name).Length - 1;
94+
string subPath = fullPath.Substring(_directory.Length + 1, length);
18395

18496
Interface.Oxide.NextTick(() =>
18597
{
186-
if (Environment.OSVersion.Platform == PlatformID.Unix)
98+
if (Regex.Match(subPath, @"include\\", RegexOptions.IgnoreCase).Success)
18799
{
188-
switch (e.ChangeType)
100+
if (e.Event == NotifyMask.OnCreated || e.Event == NotifyMask.OnModified)
189101
{
190-
case WatcherChangeTypes.Created:
191-
if (IsFileSymlink(e.FullPath))
192-
{
193-
LoadWatcherSymlink(e.FullPath);
194-
}
195-
break;
196-
197-
case WatcherChangeTypes.Deleted:
198-
if (m_symlinkWatchers.ContainsKey(e.FullPath))
199-
{
200-
m_symlinkWatchers.TryGetValue(e.FullPath, out FileSystemWatcher symlinkWatcher);
201-
symlinkWatcher?.Dispose();
202-
m_symlinkWatchers.Remove(e.FullPath);
203-
}
204-
break;
102+
FirePluginSourceChanged(subPath);
205103
}
104+
105+
return;
206106
}
207107

208-
change.timer?.Destroy();
209-
change.timer = timers.Once(.2f, () =>
108+
switch (e.Event)
210109
{
211-
change.timer = null;
212-
changeQueue.Remove(subPath);
213-
214-
if (Regex.Match(subPath, @"include\\", RegexOptions.IgnoreCase).Success)
215-
{
216-
if (change.type == WatcherChangeTypes.Created || change.type == WatcherChangeTypes.Changed)
110+
case NotifyMask.OnModified:
111+
if (watchedPlugins.Contains(subPath))
217112
{
218113
FirePluginSourceChanged(subPath);
219114
}
220-
221-
return;
222-
}
223-
224-
switch (change.type)
225-
{
226-
case WatcherChangeTypes.Changed:
227-
if (watchedPlugins.Contains(subPath))
228-
{
229-
FirePluginSourceChanged(subPath);
230-
}
231-
else
232-
{
233-
FirePluginAdded(subPath);
234-
}
235-
break;
236-
237-
case WatcherChangeTypes.Created:
115+
else
116+
{
238117
FirePluginAdded(subPath);
239-
break;
118+
}
119+
break;
240120

241-
case WatcherChangeTypes.Deleted:
242-
if (watchedPlugins.Contains(subPath))
243-
{
244-
FirePluginRemoved(subPath);
245-
}
246-
break;
247-
}
248-
});
121+
case NotifyMask.OnCreated:
122+
FirePluginAdded(subPath);
123+
break;
124+
125+
case NotifyMask.OnDeleted:
126+
if (watchedPlugins.Contains(subPath))
127+
{
128+
FirePluginRemoved(subPath);
129+
}
130+
break;
131+
}
249132
});
250133
}
251134

252-
private void watcher_Error(object sender, ErrorEventArgs e)
135+
public void OnNext(FileSystemEvent value)
253136
{
254-
Interface.Oxide.NextTick(() =>
137+
if (!value.Directory.StartsWith(_directory, StringComparison.InvariantCulture) || !filter.IsMatch(value.Name))
255138
{
256-
Interface.Oxide.LogError("FSWatcher error: {0}", e.GetException());
257-
RemoteLogger.Exception("FSWatcher error", e.GetException());
258-
});
139+
return;
140+
}
141+
142+
if ((value.Event & NotifyMask.DirectoryOnly) == NotifyMask.DirectoryOnly)
143+
{
144+
return;
145+
}
146+
147+
#if DEBUG
148+
Interface.Oxide.LogDebug($"Processing {value.Event}: {Path.Combine(value.Directory, value.Name)}");
149+
#endif
150+
watcher_Changed(watcher, value);
151+
}
152+
153+
public void OnError(Exception error)
154+
{
155+
Interface.Oxide.LogError("FSWatcher error: {0}", error);
156+
RemoteLogger.Exception("FSWatcher error", error);
157+
}
158+
159+
public void OnCompleted()
160+
{
259161
}
260162
}
261163
}

0 commit comments

Comments
 (0)