Skip to content

Commit 3c5ee5f

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 <Copilot@users.noreply.github.com>
1 parent 213a41d commit 3c5ee5f

9 files changed

Lines changed: 1272 additions & 2 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.Diagnostics;
17+
using System.Runtime.InteropServices;
18+
using Microsoft.Win32.SafeHandles;
19+
20+
internal static partial class Interop
21+
{
22+
internal static partial class PortFs
23+
{
24+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortCreate",
25+
SetLastError = true)]
26+
internal static partial SafeFileHandle PortCreate();
27+
28+
// pFileObj must point to pinned memory with size >= sizeof(file_obj)
29+
// dirPath string must remain valid for the association lifetime
30+
// mtime is the directory modification time before reading the directory
31+
// cookie is returned by PortGet to identify which directory changed
32+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortAssociate",
33+
SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
34+
internal static unsafe partial int PortAssociate(SafeFileHandle fd, IntPtr pFileObj, string dirPath, Interop.Sys.TimeSpec* mtime, uint evmask, IntPtr cookie);
35+
36+
// Returns the cookie value from PortAssociate in the cookie parameter
37+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortGet",
38+
SetLastError = true)]
39+
internal static unsafe partial int PortGet(SafeFileHandle fd, int* events, IntPtr* cookie, Interop.Sys.TimeSpec* tmo);
40+
41+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortDissociate",
42+
SetLastError = true)]
43+
internal static partial int PortDissociate(SafeFileHandle fd, IntPtr pFileObj);
44+
45+
// Send a synthetic event to wake up a blocked PortGet call
46+
// evflags can be any PortEvent value (e.g., FILE_NOFOLLOW for cancellation)
47+
// cookie is returned to PortGet to identify the event
48+
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_PortSend",
49+
SetLastError = true)]
50+
internal static partial int PortSend(SafeFileHandle fd, int evflags, IntPtr cookie);
51+
52+
[Flags]
53+
internal enum PortEvent
54+
{
55+
FILE_ACCESS = 0x00000001,
56+
FILE_MODIFIED = 0x00000002,
57+
FILE_ATTRIB = 0x00000004,
58+
FILE_TRUNC = 0x00100000,
59+
FILE_NOFOLLOW = 0x10000000,
60+
FILE_EXCEPTION = 0x20000000,
61+
62+
// Exception events
63+
FILE_DELETE = 0x00000010,
64+
FILE_RENAME_TO = 0x00000020,
65+
FILE_RENAME_FROM = 0x00000040,
66+
// These are unused, and the duplicate value causes warnings.
67+
// UNMOUNTED = 0x20000000,
68+
// MOUNTEDOVER = 0x40000000,
69+
}
70+
71+
// sizeof(file_obj) = 3*sizeof(timespec) + 3*sizeof(uintptr_t) + sizeof(char*)
72+
// On 64-bit: 3*16 + 3*8 + 8 = 80 bytes
73+
internal const int FileObjSize = 80;
74+
}
75+
}

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)