Skip to content

Address issue #478: harden file watching against transient paths and remote volumes#492

Merged
schuyler merged 1 commit into
mainfrom
claude/release-candidate-blockers-7cn2q8
Jun 30, 2026
Merged

Address issue #478: harden file watching against transient paths and remote volumes#492
schuyler merged 1 commit into
mainfrom
claude/release-candidate-blockers-7cn2q8

Conversation

@schuyler

Copy link
Copy Markdown
Owner

Summary

Closes the three file-watcher gaps identified in review of the merged PR #424 (canWatchPath: local-volume gating). The headline fix is an auto-reload regression: after an external editor's atomic rename-replace save, the locality check could disable watching and silently stop live preview updates.

Changes

  • +[MPFileWatcher canWatchPath:]NSURLVolumeIsLocalKey can only be read from a URL that resolves to an existing item. When the file is transiently missing (mid atomic save), probe the parent directory's volume instead, so auto-reload isn't wrongly disabled. Return NO only if neither the path nor its parent resolves, or the volume is non-local. Also captures the NSError from getResourceValue: (was NULL) and NSLogs it to help diagnose "Device not configured" failures.
  • -[MPResourceWatcherSet addWatcherForPath:] — added an explicit canWatchPath: guard up front, mirroring the document watcher. (Resource watchers were already filtered indirectly via MPFileWatcher's internal check + the isWatching store gate, so this is clarity/symmetry/testability, not a behavior fix.)
  • -[MPDocument startFileWatching] — moved [self stopFileWatching] to the top, above the nil/non-file-URL early return, so no early return can leak a previously-armed watcher.
  • Doc comment — rewrote the canWatchPath: header comment to describe all return-NO cases (nil/empty/non-local/unresolvable) and the parent-directory fallback.

Notes on the issue framing

Two of the three "gaps" turned out to be milder than the issue body suggested, and I've documented that on the issue:

  • Gap 1 (resource watchers unguarded) was not a live defect — remote-volume resources were already skipped gracefully. The explicit guard is added anyway for clarity.
  • Gap 3 (stale watcher) was mostly handled already — stopFileWatching ran before the canWatchPath: return; only the nil-URL guard preceded it. The reorder closes that remaining edge.

The substantive fix is Gap 2 (the atomic-save auto-reload regression).

Tests

Added to existing suites (no new files, so no Xcode target wiring needed):

  • MPFileWatcherTests: testCannotWatchEmptyPath, testCanWatchNonexistentLocalPath (the regression lock — fails against the old code), testCannotWatchPathWithNonexistentParent.
  • MPResourceWatcherSetTests: testSkipsUnwatchablePath.

Existing tests (testInitWithNonexistentPath, testSkipsNonexistentFiles) still hold: canWatchPath: now returns YES for a non-existent-but-local path, but initWithPath:'s open(O_EVTONLY) still fails for a truly absent file, so nothing is falsely watched.

Tests run on macOS CI only (per CLAUDE.md); changes verified by code reading and a fresh-eyes review in this Linux environment.

Manual Testing Plan

  1. Open a Markdown document; confirm the preview live-reloads when you edit the file in an external editor that does atomic saves (e.g. BBEdit, VS Code, or printf ... > tmp && mv tmp file.md).
  2. Repeat the external save several times in a row — auto-reload should keep working every time (previously it could silently stop after the first atomic save).
  3. Place a .md file on a remote/network volume (SMB/AFP share) and open it — the app should behave normally with live-watching simply skipped (no errors/hangs).
  4. Reference a local image/CSS in the document, then modify that resource externally — the preview should update.
  5. Use Save As to a new location, then save the original file externally — the watcher should follow the current document and not fire for the old path.

Related Issue

Related to #478


Generated by Claude Code

…remote volumes

- canWatchPath: falls back to the parent directory for the volume-locality
  check when the file is transiently missing (e.g. an external editor's
  atomic rename-replace save), so auto-reload is no longer silently disabled.
  It now also captures and logs the NSError from getResourceValue:.
- Guard MPResourceWatcherSet -addWatcherForPath: with canWatchPath: up front,
  mirroring the document watcher.
- Reorder MPDocument -startFileWatching so stopFileWatching always runs first,
  preventing a stale watcher leak on the nil/non-file-URL early return.
- Add regression tests for the empty-string, non-existent-local, and
  missing-parent canWatchPath: branches and an unwatchable resource path.
- Update the canWatchPath: header doc comment to describe all cases.

Related to #478
@schuyler schuyler force-pushed the claude/release-candidate-blockers-7cn2q8 branch from 6860df1 to 0173640 Compare June 30, 2026 05:38
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report

Current Coverage: 62.93%

Coverage Details (Summary)
Name                                                                                                                                   Coverage             
-------------------------------------------------------------------------------------------------------------------------------------- -------------------- 
MASPreferences.bundle                                                                                                                  0.00% (0/0)          
MacDown 3000.app                                                                                                                       63.93% (9320/14579)  
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/NSColor+HTML.m                                                   94.05% (332/353)     
        +[NSColor(HTML) colorWithHexString:]                                                                                           94.12% (16/17)       
        +[NSColor(HTML) colorWithHTMLName:]                                                                                            89.13% (164/184)     
        __35+[NSColor(HTML) colorWithHTMLName:]_block_invoke                                                                           100.00% (152/152)    
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Document/MPAsset.m                                                         93.69% (104/111)     
        -[MPAsset typeName]                                                                                                            100.00% (3/3)        
        -[MPAsset defaultTypeName]                                                                                                     100.00% (3/3)        
        +[MPAsset assetWithURL:andType:]                                                                                               100.00% (3/3)        
        -[MPAsset initWithURL:andType:]                                                                                                87.50% (7/8)         
        -[MPAsset init]                                                                                                                100.00% (3/3)        
        -[MPAsset templateForOption:]                                                                                                  0.00% (0/6)          
        -[MPAsset htmlForOption:]                                                                                                      100.00% (28/28)      
        -[MPStyleSheet defaultTypeName]                                                                                                100.00% (3/3)        
        +[MPStyleSheet CSSWithURL:]                                                                                                    100.00% (3/3)        
        -[MPStyleSheet templateForOption:]                                                                                             100.00% (20/20)      
        -[MPScript defaultTypeName]                                                                                                    100.00% (3/3)        
        +[MPScript javaScriptWithURL:]                                                                                                 100.00% (3/3)        
        -[MPScript templateForOption:]                                                                                                 100.00% (20/20)      
        -[MPEmbeddedScript htmlForOption:]                                                                                             100.00% (5/5)        
    /Users/runner/work/macdown3000/macdown3000/Dependency/peg-markdown-highlight/HGMarkdownHighlightingStyle.m                         89.23% (58/65)       
        +[HGMarkdownHighlightingStyle colorFromARGBColor:]                                                                             100.00% (6/6)        
        -[HGMarkdownHighlightingStyle initWithType:attributesToAdd:toRemove:fontTraitsToAdd:]                                          88.89% (8/9)         
        -[HGMarkdownHighlightingStyle initWithStyleAttributes:baseFont:]                                                               88.00% (44/50)       
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/FileURLInlining.m                                                  26.60% (25/94)       
        +[FileURLInlining inlineFromIterable:]                                                                                         0.00% (0/11)         
        -[FileURLInlining initWithURL:]                                                                                                0.00% (0/6)          
        -[FileURLInlining init]                                                                                                        100.00% (3/3)        
        -[FileURLInlining inlineContent]                                                                                               0.00% (0/9)          
        -[FileURLInlining imageContent]                                                                                                0.00% (0/12)         
        +[FileURLInlining mimeTypeForFilePath:]                                                                                        100.00% (22/22)      
        -[FileURLInlining clippingContent]                                                                                             0.00% (0/4)          
        +[FileURLInlining isTextClippingAtPath:]                                                                                       0.00% (0/12)         
        +[FileURLInlining textContentFromClipping:]                                                                                    0.00% (0/15)         
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/MPResourceWatcherSet.m                                             93.75% (90/96)       
        -[MPResourceWatcherSet init]                                                                                                   100.00% (6/6)        
        -[MPResourceWatcherSet dealloc]                                                                                                0.00% (0/3)          
        -[MPResourceWatcherSet watchedPaths]                                                                                           100.00% (3/3)        
        -[MPResourceWatcherSet updateWatchedPaths:]                                                                                    100.00% (16/16)      
        -[MPResourceWatcherSet addWatcherForPath:]                                                                                     100.00% (34/34)      
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke                                                                    100.00% (6/6)        
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke.39                                                                 93.33% (14/15)       
        __42-[MPResourceWatcherSet addWatcherForPath:]_block_invoke_2                                                                  75.00% (6/8)         
        -[MPResourceWatcherSet stopAll]                                                                                                100.00% (5/5)        
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/hoedown_html_patch.c                                             91.39% (191/209)     
        hoedown_patch_reset_checkbox_index                                                                                             100.00% (3/3)        
        new_growable_buffer                                                                                                            100.00% (3/3)        

... (3430 more lines truncated)

📊 **Full coverage report available in workflow artifacts**

@schuyler schuyler merged commit 2f62a58 into main Jun 30, 2026
10 checks passed
schuyler added a commit that referenced this pull request Jun 30, 2026
@schuyler schuyler added the rc-pending Included in a release candidate awaiting validation label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rc-pending Included in a release candidate awaiting validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants