fix: change keyfile watcher to poll instead of inotify on FAT32#1820
fix: change keyfile watcher to poll instead of inotify on FAT32#1820
Conversation
WalkthroughAdds reloadVarIniWithRetry(maxRetries) with exponential-backoff retrying of var.ini reloads until registration state (regTy) changes or retries exhausted; replaces inotify watcher with a 5s polling-based registration key watcher and adds tests validating retry paths and timing. Changes
Sequence DiagramsequenceDiagram
participant Watcher as Key Watcher (polling)
participant Logger
participant Reg as Registration loader
participant Store as Vuex Store
participant Emhttp as emhttp (loadSingleStateFile / getters)
Watcher->>Logger: log file event
Watcher->>Reg: loadRegistrationKey()
Watcher->>Store: call reloadVarIniWithRetry(maxRetries)
par Retry loop
Store->>Emhttp: read current regTy (before)
Store->>Emhttp: loadSingleStateFile(StateFileKey.var)
Emhttp-->>Store: action (state file payload)
Store->>Store: dispatch(action)
Store->>Emhttp: read regTy (after)
alt regTy changed
Store->>Logger: log change, stop retries
else regTy unchanged and retries left
Store->>Store: wait backoff (500ms → 1000ms → 2000ms)
Store->>Store: increment retry count
else max retries reached
Store->>Logger: log max retries, stop
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1820 +/- ##
==========================================
+ Coverage 52.04% 52.06% +0.02%
==========================================
Files 874 874
Lines 50372 50396 +24
Branches 5017 5014 -3
==========================================
+ Hits 26214 26238 +24
Misses 24083 24083
Partials 75 75 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
api/src/store/watch/registration-watch.ts (2)
9-32: Retry helper is correct and bounded; consider guarding against overlapping callsThe
reloadVarIniWithRetrylogic looks sound: it comparesregTybefore/after, uses bounded exponential backoff, and logs clearly. One minor improvement: because this function can be triggered for every key‑file event, you could add a simple in‑flight guard (e.g., skip starting a new retry loop if one is already running) to avoid multiple overlapping reloads hammeringloadSingleStateFileand producing noisy logs under bursty events. This is optional, since the current behavior is still correct and side‑effect safe.
34-50: Chokidar watcher config fits FAT32 needs; verifyignoredbehavior and consider a glob for clarityThe switch to polling with
usePolling: trueand a 5sintervalis appropriate for/boot/configon FAT32, and the extra logging on key events plusreloadVarIniWithRetrywiring looks good. Two minor points:
- With
ignored: (path) => !path.endsWith('.key'), depending on chokidar internals, there’s a small risk the root directory path (/boot/config) might also be treated as ignored. Please verify in manual testing that.keychanges under/boot/configreliably emit events. If there’s any flakiness, a more explicit pattern likewatch('/boot/config/*.key', { ... })(and dropping theignoredcallback) would avoid ambiguity.- If in the future you need to process an already‑present key at startup (similar to the DockerEventService behavior), you may want
ignoreInitial: falseso existing.keyfiles trigger the handler once on boot; for this PR your separate startup logic likely makesignoreInitial: truefine. Based on learnings, this mirrors how Docker’s watcher ensures existing files are processed at startup.api/src/__test__/store/watch/registration-watch.test.ts (2)
34-53: Simplify mock typing to avoid double casts inbeforeEachThe pattern:
let store: { dispatch: ReturnType<typeof vi.fn> };store = storeModule.store as unknown as typeof store;- Similar for
gettersandloadSingleStateFileworks but relies on
as unknown ascasts that the guidelines try to avoid. You can make this cleaner by declaring a typed interface for the mocked modules and casting once at import, e.g.:type MockedStoreModule = { store: { dispatch: ReturnType<typeof vi.fn> }; getters: { emhttp: ReturnType<typeof vi.fn> }; }; const storeModule = (await import('@app/store/index.js')) as MockedStoreModule; const emhttpModule = (await import('@app/store/modules/emhttp.js')) as { loadSingleStateFile: ReturnType<typeof vi.fn>; };This keeps everything strongly typed without chaining
unknowncasts.
55-149: Tests give good coverage; consider asserting withStateFileKey.varinstead of'var'The test scenarios for
reloadVarIniWithRetry(early change, max‑retry, mid‑loop change, undefinedregTy, and backoff timing) are thorough and align well with the helper’s behavior. One small robustness tweak: in the first test you assertexpect(loadSingleStateFile).toHaveBeenCalledWith('var');Since the production code calls
loadSingleStateFile(StateFileKey.var), importingStateFileKeyinto the test and asserting againstStateFileKey.varwould keep the test resilient if the enum’s underlying string value ever changes:import { StateFileKey } from '@app/store/types.js'; expect(loadSingleStateFile).toHaveBeenCalledWith(StateFileKey.var);Functionally everything is correct as‑is; this would just decouple the test from the literal enum value.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
api/src/__test__/store/watch/registration-watch.test.ts(1 hunks)api/src/store/watch/registration-watch.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
api/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer adding new files to the NestJS repo located at
api/src/unraid-api/instead of the legacy code
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
api/src/store/watch/registration-watch.tsapi/src/__test__/store/watch/registration-watch.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
api/src/__test__/store/watch/registration-watch.test.ts
api/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)
api/**/*.test.{ts,tsx}: Use Vitest for the test suite, not Jest
Prefer to not mock simple dependencies
For error testing, use.rejects.toThrow()without arguments; do not test exact error message strings unless the message format is specifically what you're testing
Files:
api/src/__test__/store/watch/registration-watch.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
api/src/__test__/store/watch/registration-watch.test.ts
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: UsecreatePiniaandsetActivePiniawhen testing Pinia store files
UsecreatePinia()instead ofcreateTestingPinia()for most Pinia store tests
Only usecreateTestingPiniaif you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when usingcreatePinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing
Files:
api/src/__test__/store/watch/registration-watch.test.ts
🧠 Learnings (11)
📚 Learning: 2025-04-07T14:34:47.255Z
Learnt from: elibosley
Repo: unraid/api PR: 1334
File: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts:63-66
Timestamp: 2025-04-07T14:34:47.255Z
Learning: In DockerEventService, the chokidar file watcher is configured with `ignoreInitial: false` to ensure that existing files (like the Docker socket) are detected and processed at application startup, not just when files change after the watcher is initialized.
Applied to files:
api/src/store/watch/registration-watch.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test async operations completely in Pinia store tests
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify proper error handling in Pinia store tests
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Applied to files:
api/src/__test__/store/watch/registration-watch.test.ts
🧬 Code graph analysis (2)
api/src/store/watch/registration-watch.ts (4)
api/src/store/index.ts (2)
getters(50-55)store(10-16)api/src/store/modules/emhttp.ts (1)
loadSingleStateFile(107-141)api/src/core/log.ts (1)
keyServerLogger(94-94)api/src/store/modules/registration.ts (1)
loadRegistrationKey(21-43)
api/src/__test__/store/watch/registration-watch.test.ts (3)
api/src/store/index.ts (2)
store(10-16)getters(50-55)api/src/store/modules/emhttp.ts (1)
loadSingleStateFile(107-141)api/src/store/watch/registration-watch.ts (1)
reloadVarIniWithRetry(15-32)
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
🤖 I have created a release *beep* *boop* --- ## [4.28.0](v4.27.2...v4.28.0) (2025-12-15) ### Features * when cancelling OS upgrade, delete any plugin files that were d… ([#1823](#1823)) ([74df938](74df938)) ### Bug Fixes * change keyfile watcher to poll instead of inotify on FAT32 ([#1820](#1820)) ([23a7120](23a7120)) * enhance dark mode support in theme handling ([#1808](#1808)) ([d6e2939](d6e2939)) * improve API startup reliability with timeout budget tracking ([#1824](#1824)) ([51f025b](51f025b)) * PHP Warnings in Management Settings ([#1805](#1805)) ([832e9d0](832e9d0)) * update @unraid/shared-callbacks to version 3.0.0 ([#1831](#1831)) ([73b2ce3](73b2ce3)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
Changes
Test plan
Summary by CodeRabbit
Tests
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.