Skip to content

Commit 957c324

Browse files
committed
Add Unix FileSystemWatcher
1 parent 24b7e69 commit 957c324

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

src/IO/Unix/UnixFileSystem.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
extern alias References;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Text;
6+
using References::Mono.Unix.Native;
7+
8+
namespace Oxide.IO.Unix
9+
{
10+
internal class UnixFileSystem : IFileSystem
11+
{
12+
private const int MAX_BUFFERS = 5;
13+
private const int MAX_PATH_BUFFER = 4096;
14+
private Queue<StringBuilder> StrBuffers { get; } = new Queue<StringBuilder>();
15+
16+
public bool IsSymbolicLink(string path)
17+
{
18+
int result = Syscall.lstat(path, out Stat buffer);
19+
20+
if (result == -1)
21+
{
22+
Errno err = Stdlib.GetLastError();
23+
24+
switch (err)
25+
{
26+
default:
27+
return false;
28+
29+
case Errno.EACCES:
30+
throw new AccessViolationException("Search permission is denied for a component of the path prefix.");
31+
}
32+
}
33+
34+
return (buffer.st_mode & FilePermissions.S_IFMT) == FilePermissions.S_IFMT;
35+
}
36+
37+
public string ResolvePath(string path)
38+
{
39+
if (!IsSymbolicLink(path))
40+
{
41+
return Path.GetFullPath(path);
42+
}
43+
44+
StringBuilder str = null;
45+
46+
lock (StrBuffers)
47+
{
48+
str = StrBuffers.Count == 0 ? new StringBuilder(MAX_PATH_BUFFER) : StrBuffers.Dequeue();
49+
}
50+
51+
try
52+
{
53+
int result = Syscall.readlink(path, str, MAX_PATH_BUFFER);
54+
55+
if (result != -1)
56+
{
57+
return str.ToString();
58+
}
59+
60+
Errno err = Stdlib.GetLastError();
61+
throw new IOException($"Unix filesystem returned: {err}");
62+
}
63+
finally
64+
{
65+
lock (StrBuffers)
66+
{
67+
if (StrBuffers.Count < MAX_BUFFERS)
68+
{
69+
str.Length = 0;
70+
StrBuffers.Enqueue(str);
71+
}
72+
}
73+
}
74+
}
75+
}
76+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
extern alias References;
2+
using System;
3+
using System.IO;
4+
using System.Runtime.InteropServices;
5+
using System.Text;
6+
using System.Threading;
7+
using Oxide.Core;
8+
using References::Mono.Unix.Native;
9+
10+
namespace Oxide.IO.Unix
11+
{
12+
internal class UnixFileSystemWatcher : CachedFileSystemWatcher
13+
{
14+
#region Structures
15+
16+
[StructLayout(LayoutKind.Sequential)]
17+
private struct InotifyEvent
18+
{
19+
public int Descriptor;
20+
public NotifyMask Mask;
21+
public uint Cookie;
22+
public uint Length;
23+
}
24+
25+
#endregion
26+
27+
private readonly int fileDescriptor;
28+
private readonly Thread worker;
29+
private volatile bool is_working;
30+
31+
[DllImport("libc", SetLastError = true)]
32+
private static extern int inotify_init();
33+
34+
[DllImport("libc", SetLastError = true)]
35+
private static extern int inotify_add_watch(int fd, string pathname, uint mask);
36+
37+
[DllImport("libc", SetLastError = true)]
38+
private static extern int inotify_rm_watch(int fd, int wd);
39+
40+
[DllImport("libc", SetLastError = true)]
41+
private static extern int read(int fd, byte[] buffer, int count);
42+
43+
[DllImport("libc", SetLastError = true)]
44+
private static extern int close(int fd);
45+
46+
#region Initialization
47+
48+
public UnixFileSystemWatcher(IFileSystem fs, string directory, bool subDirs, NotifyMask filter, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) : base(directory, subDirs, filter, fs, stringComparison)
49+
{
50+
fileDescriptor = inotify_init();
51+
worker = new Thread(DoWork)
52+
{
53+
Name = "Oxide_" + nameof(UnixFileSystemWatcher),
54+
IsBackground = true,
55+
CurrentCulture = Thread.CurrentThread.CurrentCulture,
56+
CurrentUICulture = Thread.CurrentThread.CurrentUICulture
57+
};
58+
59+
if (fileDescriptor != -1)
60+
{
61+
is_working = true;
62+
return;
63+
}
64+
65+
Errno err = Stdlib.GetLastError();
66+
throw new IOException($"Failed to initialize inotify: {err}", (int)err);
67+
}
68+
69+
~UnixFileSystemWatcher()
70+
{
71+
ReleaseUnmanagedResources();
72+
}
73+
74+
public override void EndInit()
75+
{
76+
base.EndInit();
77+
worker.Start();
78+
}
79+
80+
#endregion
81+
82+
#region FileSystem Dependent
83+
84+
protected override int SubscribeTo(string directory)
85+
{
86+
int result = inotify_add_watch(fileDescriptor, directory, (uint)Filter);
87+
88+
if (result != -1)
89+
{
90+
LogDebug($"Subscribed to inotify handle {result} | {directory}");
91+
return result;
92+
}
93+
94+
Errno err = Stdlib.GetLastError();
95+
96+
try
97+
{
98+
throw new IOException($"Failed to subscribe to '{directory}'", result);
99+
}
100+
catch (Exception e)
101+
{
102+
OnFileSystemError(e);
103+
throw;
104+
}
105+
}
106+
107+
protected override bool UnsubscribeFrom(int id)
108+
{
109+
int result = inotify_rm_watch(fileDescriptor, id);
110+
111+
if (result != -1)
112+
{
113+
LogDebug($"Unsubscribed from inotify handle '{id}'");
114+
115+
}
116+
117+
return true;
118+
}
119+
120+
#endregion
121+
122+
private void DoWork()
123+
{
124+
byte[] buffer = new byte[4096];
125+
126+
while (is_working)
127+
{
128+
int length = read(fileDescriptor, buffer, buffer.Length);
129+
130+
if (length > 0)
131+
{
132+
for (int i = 0; i < length;)
133+
{
134+
uint size = 0;
135+
string name = null;
136+
string parent = null;
137+
InotifyEvent evt = default;
138+
try
139+
{
140+
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
141+
IntPtr n = new IntPtr(handle.AddrOfPinnedObject().ToInt64() + i);
142+
evt = (InotifyEvent)Marshal.PtrToStructure(n, typeof(InotifyEvent));
143+
size = evt.Length;
144+
145+
if (evt.Length > 0)
146+
{
147+
name = Encoding.UTF8
148+
.GetString(buffer, i + Marshal.SizeOf(typeof(InotifyEvent)), (int)size)
149+
.TrimEnd('\0');
150+
}
151+
handle.Free();
152+
153+
parent = GetDirectoryById(evt.Descriptor);
154+
OnFileSystemEvent(parent, name, evt.Mask);
155+
}
156+
catch (Exception e)
157+
{
158+
Interface.Oxide.LogException($"Failed to read change | Parent: {parent}, Name: {name}, {evt.Mask}", e);
159+
OnFileSystemError(e);
160+
}
161+
finally
162+
{
163+
i += Marshal.SizeOf(typeof(InotifyEvent)) + (int)size;
164+
}
165+
}
166+
}
167+
}
168+
}
169+
170+
protected override void OnFileSystemEvent(string directory, string name, NotifyMask mask)
171+
{
172+
directory = FileSystem.ResolvePath(directory);
173+
string fullPath = string.IsNullOrEmpty(name) ? directory : Path.Combine(directory, name);
174+
CleanName(fullPath, out directory, out name);
175+
base.OnFileSystemEvent(directory, name, mask);
176+
}
177+
178+
private void ReleaseUnmanagedResources()
179+
{
180+
is_working = false;
181+
worker.Join();
182+
close(fileDescriptor);
183+
}
184+
185+
protected override void OnDispose()
186+
{
187+
ReleaseUnmanagedResources();
188+
GC.SuppressFinalize(this);
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)