Skip to content

Add filtered Windows watcher operations - #6

Draft
SearidangPa wants to merge 1 commit into
preveilfrom
da-8762-separate-windows-notify-ops
Draft

SearidangPa wants to merge 1 commit into
preveilfrom
da-8762-separate-windows-notify-ops

Conversation

@SearidangPa

@SearidangPa SearidangPa commented Aug 25, 2026 •

Copy link
Copy Markdown

Summary

  • add a Windows-only Watcher.AddWithOps API for selecting fsnotify operations
  • allow FILE_ACTION_MODIFIED to map to Chmod for attributes-only subscriptions
  • preserve the existing Write mapping for legacy combined Add subscriptions
  • add Windows tests covering mask conversion and separate attribute/write watchers

Motivation

ReadDirectoryChangesW reports both FILE_NOTIFY_CHANGE_LAST_WRITE and FILE_NOTIFY_CHANGE_ATTRIBUTES as FILE_ACTION_MODIFIED. When both filters are used by one watcher, the backend cannot tell which filter matched.

Using independent watchers with AddWithOps(..., Write) and AddWithOps(..., Chmod) lets consumers distinguish the subscription that fired. This is needed by the Windows cloud-drive watcher to separate placeholder pin-state metadata from content-write notifications.

Compatibility

The existing Add behavior is unchanged: combined modified notifications continue to be emitted as Write.

The fsnotify branch does not detect “pin” directly. It separates two kinds of Windows filesystem notifications so the caller can interpret them differently.

The original behavior

The current Windows backend effectively does this:

fsnotify.Add(root)
        |
        v
ReadDirectoryChangesW(
    LAST_WRITE |
    ATTRIBUTES |
    FILE_NAME changes
)
        |
        v
Windows reports FILE_ACTION_MODIFIED
        |
        v
fsnotify always emits Write

The important problem is that Windows reports both:

  • FILE_NOTIFY_CHANGE_LAST_WRITE
  • FILE_NOTIFY_CHANGE_ATTRIBUTES

as the same raw action:

FILE_ACTION_MODIFIED

So the old backend loses the distinction and always converts it to:

fsnotify.Write

That is why pin operations currently look like local writes.


What the fsnotify branch adds

1. A filtered watch API

The branch adds:

func (w *Watcher) AddWithOps(name string, ops Op) error

Example:

watcher.AddWithOps(root, fsnotify.Write)

or:

watcher.AddWithOps(root, fsnotify.Chmod)

Internally, it converts the public fsnotify operations into Windows notification filters:

fsnotify.Write  -> FILE_NOTIFY_CHANGE_LAST_WRITE
fsnotify.Chmod  -> FILE_NOTIFY_CHANGE_ATTRIBUTES
fsnotify.Create -> file/directory name changes
fsnotify.Remove -> file/directory name changes
fsnotify.Rename -> file/directory name changes

Chmod is being used because it is the existing fsnotify operation that represents filesystem attribute changes. On Windows, this means FILE_NOTIFY_CHANGE_ATTRIBUTES; it does not necessarily mean a Unix-style permission change.


2. Modified events can now be emitted as Chmod

When Windows reports:

FILE_ACTION_MODIFIED

the backend looks at the filter used for that watch:

if the watch requested attributes only:
    emit fsnotify.Chmod
else:
    emit fsnotify.Write

So the backend can now produce:

attribute-only watcher -> Chmod
last-write watcher      -> Write
legacy combined watcher -> Write

The existing Add() API is intentionally preserved:

watcher.Add(root)

still behaves as before and treats modified events as Write.


Why two fsnotify watchers are required

This is the most important part.

This is not sufficient:

w := fsnotify.NewWatcher()

w.AddWithOps(root, fsnotify.Write)
w.AddWithOps(root, fsnotify.Chmod)

Those filters are merged onto the same underlying Windows watch. The resulting ReadDirectoryChangesW request contains both filters, and Windows still only reports:

FILE_ACTION_MODIFIED

At that point, the backend cannot know which filter caused the event.

Instead, the caller must create two independent fsnotify watchers:

contentWatcher, _ := fsnotify.NewWatcher()
attributeWatcher, _ := fsnotify.NewWatcher()

contentWatcher.AddWithOps(
    root,
    fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename,
)

attributeWatcher.AddWithOps(root, fsnotify.Chmod)

This produces two separate Windows requests:

contentWatcher:
    LAST_WRITE + file/dir name changes

attributeWatcher:
    ATTRIBUTES only

Then the application reads from both channels and combines them into its own event pipeline.

That is what the corresponding changes in this repository do.


Actual event behavior

Pinning a placeholder

The Windows test showed:

attribute watcher: Chmod
content watcher:   no event

The local changes watcher then interprets Chmod as:

PinStateChange

It reads the Cloud Files placeholder metadata to determine whether the state is:

  • pinned
  • unpinned
  • unspecified

The fsnotify library itself does not determine which pin state occurred.


Writing a normal file

The test showed:

content watcher:   Write
attribute watcher: no event

This is treated as a content update.


Writing a placeholder

This is the caveat:

content watcher:   Write
attribute watcher: Chmod

Placeholder writes modify both file content and placeholder/in-sync metadata. Therefore, separating the notification streams does not guarantee exactly one event per operation.

The application still needs logic to ignore the attribute event when it is merely a side effect of a content write. The current local changes handler does that by comparing the file modification time against the placeholder identity metadata.


What the branch does and does not solve

It solves

  • The backend no longer has to treat every Windows modification as a content write.
  • Callers can subscribe specifically to last-write notifications.
  • Callers can subscribe specifically to attribute notifications.
  • Existing fsnotify.Add() users remain compatible.

It does not solve

  • It does not emit Pinned or Unpinned directly.
  • It cannot identify the exact attribute that changed.
  • An attribute event is not automatically proof of a pin-state change.
  • Placeholder writes can still produce both a write and an attribute event.
  • The caller must use separate fsnotify.Watcher instances.

The intended flow is therefore:

Windows attribute notification
        |
        v
fsnotify.Chmod
        |
        v
local watcher PinStateChange
        |
        v
CfGetPlaceholderInfo
        |
        v
Pinned / Unpinned behavior

Tests

  • go test ./... on Windows
  • verified an attributes-only watcher receives Chmod but not Write
  • verified a last-write-only watcher receives Write but not Chmod

@SearidangPa
SearidangPa marked this pull request as draft August 25, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant