Skip to content

Implement SunOS FileSystemWatcher using portfs event ports#124716

Open
gwr wants to merge 3 commits intodotnet:mainfrom
gwr:illumos-fswatch
Open

Implement SunOS FileSystemWatcher using portfs event ports#124716
gwr wants to merge 3 commits intodotnet:mainfrom
gwr:illumos-fswatch

Conversation

@gwr
Copy link
Contributor

@gwr gwr commented Feb 22, 2026

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


Copilot AI review requested due to automatic review settings February 22, 2026 01:54
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Feb 22, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements FileSystemWatcher support for SunOS (Solaris/illumos) using the portfs (event ports) API. Unlike Linux's inotify which reports specific file changes, portfs only signals that a directory has changed, requiring the implementation to maintain snapshots and perform comparisons to determine what actually changed.

Changes:

  • Native interop layer for portfs operations (port_create, port_associate, port_get, port_send, port_dissociate)
  • SunOS-specific FileSystemWatcher implementation using snapshot-based change detection
  • Hybrid monitoring mode that watches both directories (for name changes) and individual files (for attribute changes)
  • Support for recursive subdirectory monitoring with resource limits (50 subdirs, 1000 files per directory)

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src/native/libs/configure.cmake CMake configuration for portfs feature detection (has critical bug)
src/native/libs/System.Native/pal_io.h Header declarations for portfs native functions (has spelling errors)
src/native/libs/System.Native/pal_io.c Native implementation of portfs wrapper functions (has critical string lifetime bug)
src/native/libs/System.Native/entrypoints.c Export table entries for new portfs functions
src/native/libs/Common/pal_config.h.in Configuration flag for HAVE_SUNOS_PORTFS
src/libraries/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj Added illumos and solaris to test target frameworks
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.SunOS.cs Main SunOS implementation with snapshot comparison and event generation (multiple issues)
src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj Added SunOS-specific source files and target frameworks
src/libraries/Common/src/Interop/SunOS/portfs/Interop.portfs.cs Managed P/Invoke declarations for portfs (has hardcoded struct size issue)

@gwr gwr force-pushed the illumos-fswatch branch 2 times, most recently from cbe385d to 04c54a8 Compare February 22, 2026 02:19
Copilot AI review requested due to automatic review settings February 22, 2026 02:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 13 comments.

Copilot AI review requested due to automatic review settings February 22, 2026 02:54
@gwr gwr force-pushed the illumos-fswatch branch 2 times, most recently from 8b4f6e2 to 3c5ee5f Compare February 22, 2026 03:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.

Copilot AI review requested due to automatic review settings February 22, 2026 03:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

@jkotas jkotas added the os-SunOS SunOS, currently not officially supported label Feb 22, 2026
Copilot AI review requested due to automatic review settings February 22, 2026 05:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

gwr added a commit to gwr/dotnet-runtime that referenced this pull request Feb 22, 2026
@gwr
Copy link
Contributor Author

gwr commented Feb 22, 2026

Now that I seem to be caught-up with Copilot feedback, I've squashed so I can more easily cherry pick this into a local branch with other necessary fixes so I can re-test.

@am11
Copy link
Member

am11 commented Feb 22, 2026

@tmds, could you please take a look?

Portfs is a generic I/O eventing facility in SunOS-like platforms: port_create(3C) - https://smartos.org/man/3c/port_create. I might be mistaken, but regarding time complexity: with your recent Linux inotify overhaul, the kernel:userland path is essentially O(1). On illumos/Solaris, this implemention's kernel:userland portion is O(n log n) due to the snapshot comparison required to reconstruct file-level changes. This complexity is probably unavoidable given the lack of per-file events, but (perhaps?) still better than not having any watcher support.

Copilot AI review requested due to automatic review settings February 23, 2026 03:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings February 23, 2026 04:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

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 99% of System.IO.FileSystem.Watcher.Tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@gwr
Copy link
Contributor Author

gwr commented Feb 23, 2026

Squashed the last dozen updates to simplify local testing.

@gwr
Copy link
Contributor Author

gwr commented Feb 26, 2026

Can anyone help me interpret the CI build analysis?
Is there anything there I need to fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I wondered if I should add functions like this to pal_io.c or create a new pal_portfs.c or pal_sunos.c or something for the SunOS-specific additions. Guidance? Thanks.

@gwr
Copy link
Contributor Author

gwr commented Mar 3, 2026

Still needs a reviewer. Thanks.

@am11
Copy link
Member

am11 commented Mar 3, 2026

To me it looks good. I had a question from @tmds about the overhead #124716 (comment). If the plan is to switch to inotify in the future when it gets stable upstream, we can treat it as a stopgap and merge this. Thanks for pushing it forward @gwr!

@gwr
Copy link
Contributor Author

gwr commented Mar 3, 2026

The only test failure is name="System.IO.Tests.FileSystemWatcherTests_netstandard17.EndInit_ResumesPausedEnableRaisingEvents(setBeforeBeginInit: False)" which has active issue #103584 so I think that's OK.

@tmds
Copy link
Member

tmds commented Mar 3, 2026

This complexity is probably unavoidable given the lack of per-file events, but (perhaps?) still better than not having any watcher support.

It sounds like it is implemented as good as the kernel allows.

Without this, it's likely an app is broken in some way, so I also consider it better to have this than to throw PNSE.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.

Comment on lines +1002 to +1004
foreach (var (name, watch) in _nameToWatchMap)
{
unsafe
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foreach (var (name, watch) in _nameToWatchMap) doesn't use name. With TreatWarningsAsErrors=true, this unused deconstruction local will fail the build. Iterate _nameToWatchMap.Values (or similar) to avoid declaring an unused variable.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +221 to +230
// FileName/DirectoryName: detect when entries are added/removed
if ((filters & (NotifyFilters.FileName | NotifyFilters.DirectoryName)) != 0)
mask |= (int)Interop.PortFs.PortEvent.FILE_MODIFIED;

// LastWrite/Size: detect content/size changes
if ((filters & (NotifyFilters.LastWrite | NotifyFilters.Size)) != 0)
{
mask |= (int)Interop.PortFs.PortEvent.FILE_MODIFIED;
mask |= (int)Interop.PortFs.PortEvent.FILE_TRUNC;
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetPortEventMask adds FILE_MODIFIED when NotifyFilters.FileName/DirectoryName are set. That makes sense for the directory association (detect entry add/remove), but when this same mask is used for individual file associations it can cause spurious Changed events for content/mtime updates even if the user didn't request LastWrite/Size. Consider using separate masks for directory-vs-file associations (or only enabling FILE_MODIFIED for per-file watches when LastWrite/Size are requested).

Copilot uses AI. Check for mistakes.

// File still exists - generate change event
// The event mask we used in PortAssociate already filtered to only
// the attributes we care about based on NotifyFilters
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleFileEvent ignores the events bitmask returned by port_get and always raises WatcherChangeTypes.Changed. This prevents filtering to only the NotifyFilters the user asked for (e.g., distinguishing FILE_ATTRIB vs FILE_MODIFIED vs FILE_ACCESS) and compounds the risk of emitting Changed when only name filters were intended to drive directory diffs. Use the events flags to decide whether to raise a Changed event (and which NotifyFilters it satisfies) before notifying.

Suggested change
// the attributes we care about based on NotifyFilters
// the attributes we care about based on NotifyFilters
if ((events & _portEventMask) == 0)
{
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +923 to +930
// report error and return. The event loop will naturally exit when
// no more events arrive since the directory is no longer associated.
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
watcher.OnError(new ErrorEventArgs(
Interop.GetExceptionForIoErrno(error, _directoryPath)));
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ReassociateDirectory fails, the code reports an error but keeps the event loop alive with an infinite PortGet(..., null) wait. With no directory association, no further events may arrive, so this thread can block forever and leak the port/resources until the user explicitly stops the watcher. Consider canceling the instance (or otherwise waking and exiting the loop) when reassociation fails so the watcher shuts down deterministically after a terminal error.

Suggested change
// report error and return. The event loop will naturally exit when
// no more events arrive since the directory is no longer associated.
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
watcher.OnError(new ErrorEventArgs(
Interop.GetExceptionForIoErrno(error, _directoryPath)));
}
// report an error and cancel this instance so the event loop wakes and
// shuts down deterministically rather than blocking indefinitely.
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
watcher.OnError(new ErrorEventArgs(
Interop.GetExceptionForIoErrno(error, _directoryPath)));
}
CancellationCallback();

Copilot uses AI. Check for mistakes.
Comment on lines +1564 to +1568
// Use the provided mtime from managed code
// This is the mtime captured before reading the directory
fo->fo_mtime.tv_sec = mtime->tv_sec;
fo->fo_mtime.tv_nsec = mtime->tv_nsec;
fo->fo_name = (char *)dirPath;
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SystemNative_PortAssociate dereferences mtime and assigns fo->fo_name = dirPath without validating either pointer, but the header comment describes both as optional. Either make these parameters required (update the comment + add asserts) or handle null values safely to avoid a potential native null-deref if a caller ever passes null.

Copilot uses AI. Check for mistakes.
Comment on lines +789 to +797
* pFileObj - A pointer identifying the event source (for example, a file object).
* dirPath - Optional directory path used to identify the event source.
* mtime - Optional pointer to the last modification time of the event source.
* events - A bitmask of events to monitor for the event source.
* cookie - An opaque value returned to the caller when an event is delivered.
*
* Returns 0 on success, or -1 if an error occurred (in which case, errno is set appropriately).
*/
PALEXPORT int32_t SystemNative_PortAssociate(intptr_t port, intptr_t pFileObj, const char *dirPath, TimeSpec *mtime, int32_t events, uintptr_t cookie);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs for SystemNative_PortAssociate say dirPath and mtime are optional, but the current native implementation requires them (it unconditionally uses both). Please align the contract with reality: either update this comment to mark them required (and consider adding asserts) or update the native code to tolerate null pointers.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO community-contribution Indicates that the PR has been added by a community member os-SunOS SunOS, currently not officially supported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants