Skip to content

Commit 8821346

Browse files
gwrCopilot
andcommitted
Implement SunOS FileSystemWatcher using portfs event ports
This implementation uses SunOS portfs (event ports) to watch for directory changes. Unlike Linux inotify, portfs can only detect WHEN a directory changes, not WHAT changed. Therefore, this implementation: 1. Maintains a snapshot of each watched directory's contents 2. When portfs signals a change, re-reads the directory 3. Compares new vs old snapshots to detect creates/deletes/modifications 4. Generates appropriate FileSystemWatcher events Key implementation details: - Uses pinned GC.AllocateArray for file_obj structures required by portfs - When watching attributes or timestamps watches directory contents too - Each watched objct gets a unique cookie for identification - port_get returns the cookie to indicate which object changed - Optimized snapshot comparison with sorted single-pass algorithm - Supports IncludeSubdirectories by recursively watching child directories - Track mtime of the watched directory to avoid missing changes - Implement graceful cancellation using PortSend Native changes (pal_io.c): - SystemNative_PortCreate: Opens event port file descriptor - SystemNative_PortAssociate: Associates directory with port using file_obj - SystemNative_PortGet: Waits for events, returns cookie identifying directory - SystemNative_PortSend: Send an event (used in cancellation) - SystemNative_PortDissociate: Removes directory association Performance notes: - Less efficient than inotify (must re-read directories on each change) - Better than polling (blocks until change occurs) - Memory overhead for directory snapshots Passes all System.IO.FileSystem.Watcher.Tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 213a41d commit 8821346

File tree

10 files changed

+1385
-2
lines changed

10 files changed

+1385
-2
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// This implementation uses SunOS portfs (event ports) to watch directories.
5+
// portfs can detect when a directory's modification time changes via port_associate,
6+
// but cannot tell us WHAT changed in the directory. This makes it different from
7+
// Linux inotify or Windows ReadDirectoryChangesW.
8+
//
9+
// The FileSystemWatcher implementation must:
10+
// 1. Use port_associate to watch for FILE_MODIFIED events on directories
11+
// 2. When an event occurs, re-read the directory contents
12+
// 3. Compare with cached state to determine what actually changed
13+
//
14+
15+
using System;
16+
using System.Runtime.InteropServices;
17+
using Microsoft.Win32.SafeHandles;
18+
19+
internal static partial class Interop
20+
{
21+
internal static partial class PortFs
22+
{
23+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortCreate",
24+
SetLastError = true)]
25+
internal static partial SafeFileHandle PortCreate();
26+
27+
// pFileObj must point to pinned memory with size >= sizeof(file_obj)
28+
// because the address is used as an identifier in the kernel.
29+
// dirPath string is used while making the association but is
30+
// never referenced again after PortAssociate returns.
31+
// mtime is the directory modification time before reading the directory
32+
// cookie is returned by PortGet to identify which directory changed
33+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortAssociate",
34+
SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
35+
internal static unsafe partial int PortAssociate(SafeFileHandle fd, IntPtr pFileObj, string dirPath, Interop.Sys.TimeSpec* mtime, int evmask, IntPtr cookie);
36+
37+
// Returns the cookie value from PortAssociate in the cookie parameter
38+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortGet",
39+
SetLastError = true)]
40+
internal static unsafe partial int PortGet(SafeFileHandle fd, int* events, IntPtr* cookie, Interop.Sys.TimeSpec* tmo);
41+
42+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortDissociate",
43+
SetLastError = true)]
44+
internal static partial int PortDissociate(SafeFileHandle fd, IntPtr pFileObj);
45+
46+
// Send a synthetic event to wake up a blocked PortGet call
47+
// evflags can be any PortEvent value (e.g., FILE_NOFOLLOW for cancellation)
48+
// cookie is returned to PortGet to identify the event
49+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortSend",
50+
SetLastError = true)]
51+
internal static partial int PortSend(SafeFileHandle fd, int evflags, IntPtr cookie);
52+
53+
[Flags]
54+
internal enum PortEvent
55+
{
56+
FILE_ACCESS = 0x00000001,
57+
FILE_MODIFIED = 0x00000002,
58+
FILE_ATTRIB = 0x00000004,
59+
FILE_TRUNC = 0x00100000,
60+
FILE_NOFOLLOW = 0x10000000,
61+
FILE_EXCEPTION = 0x20000000,
62+
63+
// Exception events
64+
FILE_DELETE = 0x00000010,
65+
FILE_RENAME_TO = 0x00000020,
66+
FILE_RENAME_FROM = 0x00000040,
67+
// These are unused, and the duplicate value causes warnings.
68+
// UNMOUNTED = 0x20000000,
69+
// MOUNTEDOVER = 0x40000000,
70+
}
71+
72+
// sizeof(file_obj) = 3*sizeof(timespec) + 3*sizeof(uintptr_t) + sizeof(char*)
73+
// On 64-bit: 3*16 + 3*8 + 8 = 80 bytes
74+
internal const int FileObjSize = 80;
75+
}
76+
}

src/libraries/System.IO.FileSystem.Watcher/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@
125125
<data name="FSW_BufferOverflow" xml:space="preserve">
126126
<value>Too many changes at once in directory:{0}.</value>
127127
</data>
128+
<data name="FSW_MaxFilesWatchedExceeded" xml:space="preserve">
129+
<value>Max file watch limit ({0}) exceeded for directory: {1}</value>
130+
</data>
131+
<data name="FSW_MaxSubdirectoriesExceeded" xml:space="preserve">
132+
<value>Max subdirectory watch limit ({0}) exceeded for directory: {1}</value>
133+
</data>
128134
<data name="InvalidDirName" xml:space="preserve">
129135
<value>The directory name {0} is invalid.</value>
130136
</data>

src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)</TargetFrameworks>
4+
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent)</TargetFrameworks>
55
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
66
<UseCompilerGeneratedDocXmlFile>false</UseCompilerGeneratedDocXmlFile>
77
</PropertyGroup>
@@ -89,6 +89,20 @@
8989
Link="Common\Interop\Unix\Interop.Stat.cs" />
9090
</ItemGroup>
9191

92+
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'illumos' or '$(TargetPlatformIdentifier)' == 'solaris'">
93+
<Compile Include="System\IO\FileSystemWatcher.SunOS.cs" />
94+
<Compile Include="$(CommonPath)Interop\SunOS\portfs\Interop.portfs.cs"
95+
Link="Common\Interop\SunOS\portfs\Interop.portfs.cs" />
96+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.UTimensat.cs"
97+
Link="Common\Interop\Unix\System.Native\Interop.UTimensat.cs" />
98+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
99+
Link="Common\Interop\Unix\Interop.Poll.cs" />
100+
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
101+
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
102+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Stat.cs"
103+
Link="Common\Interop\Unix\Interop.Stat.cs" />
104+
</ItemGroup>
105+
92106
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'maccatalyst'">
93107
<Compile Include="System\IO\FileSystemWatcher.OSX.cs" />
94108
<Compile Include="$(CoreLibSharedDir)System\IO\FileSystem.Exists.Unix.cs"

0 commit comments

Comments
 (0)