mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
Closes the Linux fsnotify race that drops IN_MOVED_TO events for
atomic-rename writes (`tmp + rename` shape) when the watcher has
been alive across an earlier write to the same parent directory.
Per-issue fix B.
Symptom (before this fix)
-------------------------
A second client subscribed to fs.changed (recursive root) misses
`modified` notifications for files modified by the daemon's own
fs.write / fs.writeBinary / fs.copy handlers. Surfaced by the
M6 perf bench's `modify` cell and the M9 sync-reflect E2E case
(both currently `it.skip`'d).
Fix B
-----
After atomicWriteFile succeeds, if the destination existed before
the write, the handler emits a synthetic `Modified` event into the
watcher. Subscribers cannot tell injected events apart from real
fsnotify-derived ones — that's the contract.
Implementation
--------------
server/internal/watcher/watcher.go
- New public method Watcher.Inject(event Event) — forwards to the
existing private emit() helper that all real fsnotify events
flow through.
server/internal/handlers/common.go
- atomicWriteFile signature widened from (int64, *rpc.Error) to
(int64, bool, *rpc.Error). The new bool is wasModify (true when
the destination existed before the write, false on fresh create).
- Probe is one upfront os.Stat that doubles as the existing
expectedMtime precondition check.
server/internal/handlers/fs_write.go, fs_write_binary.go, fs_copy.go
- Constructor signatures take an optional onModify func(relPath
string) callback. When wasModify && onModify != nil, fires with
the vault-relative path.
server/cmd/obsidian-remote-server/main.go
- Wires injectModify closure that calls vaultWatcher.Inject({
Path: relPath, Type: watcher.EventModified }) and passes it
into all three atomic-rename handlers.
server/internal/handlers/fs_write_test.go
- 4 unit tests covering the synthetic-emit contract for FsWrite /
FsWriteBinary / FsCopy (overwrite vs fresh-create).
- 1 watcher-integrated test that constructs a real watcher.Watcher,
subscribes root-recursive, wires the production-shape inject
closure, and waits up to 500ms for the Modified event to be
dispatched. Mirrors what main.go does at runtime.
Test infrastructure
-------------------
Existing test fixtures (server_test.go, correlation_test.go,
fs_mutate_test.go) updated to pass nil for the new onModify param —
the synthetic-emit contract is covered by the dedicated tests above.
Verification
------------
- go test ./... — all packages pass (~9s)
- npm run lint — 0 errors, 0 warnings
- npx tsc --noEmit — clean
- npx vitest run — 579/579 pass
Out of scope (deferred, per issue spec)
---------------------------------------
- M9 sync.e2e.test.ts has no `it.skip('modify')` to flip back; the
modify case isn't even present in the M9 MVP slice. A follow-up
PR can add it explicitly.
- M6 perf.sync.bench.test.ts OPS array — bench was rated lower
priority; doesn't block this fix.
What this doesn't fix
---------------------
- An out-of-band writer (user typing on the remote shell) still
relies on fsnotify and isn't routed through atomicWriteFile.
That's a niche case; left for a follow-up if needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| watcher.go | ||
| watcher_test.go | ||