Skip to content

fix(project): Add polling file watcher for containers - #1511

Draft
RandomByte wants to merge 1 commit into
mainfrom
fix/fs-watch-in-containers
Draft

fix(project): Add polling file watcher for containers#1511
RandomByte wants to merge 1 commit into
mainfrom
fix/fs-watch-in-containers

Conversation

@RandomByte

@RandomByte RandomByte commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

Add a polling file watcher for the incremental build. It is used automatically inside containers and can be forced anywhere with UI5_WATCH_MODE.

Why

On a bind-mounted volume inside a container, Linux inotify reports the container's own writes but not writes made to the same volume from outside the container. Podman is the common case. The incremental build derives "what changed" solely from @parcel/watcher events, so those missed events break rebuilds and live reload, with no error to catch. Polling reads the tree directly, so it sees every change regardless of where it originated.

How it works

A new module, fileWatcher.js, selects the backend and exposes a subscribe() matching @parcel/watcher's exact contract, so a caller cannot tell which backend is active. All three watcher consumers (WatchHandler, ProjectDefinitionWatcher, projectGraphSettleWatcher) call it instead of @parcel/watcher directly. The native backend stays @parcel/watcher; the polling backend lives in a separate pollingWatcher.js that fileWatcher.js imports lazily, only when polling is selected, so the module stays off the common native path.

Both backends are imported lazily. @parcel/watcher resolves a native binding at load time and throws when the platform's prebuilt binary is not installed (for example on an unsupported platform or after an install with --omit=optional), so a static import would break every consumer before the polling fallback could run. Loading it on demand keeps that failure contained: when the native backend is selected but cannot load, subscribe() logs a warning and falls back to polling, which needs no native code.

The backend is chosen once per process and memoized:

  • UI5_WATCH_MODE=polling|native forces it.
  • Otherwise polling is the default inside a container, native elsewhere.
  • A container is detected from the runtime marker files (/.dockerenv, /run/.containerenv) and PID 1's cgroup.

Container detection is a heuristic: it can miss an exotic sandbox or match a host whose PID 1 cgroup carries those names, so UI5_WATCH_MODE is the escape hatch either way.

The polling path walks the tree and diffs an mtimeMs+size snapshot every 250 ms (rescheduling after each walk so a slow crawl cannot overlap the next), emitting the same {type, path} events the native backend would. Errors flow through the callback so each consumer's existing recovery path fires unchanged. The 250 ms interval stays below WATCHER_BURST_SETTLE_MS (550 ms) so downstream event batching still holds. Ignore globs reuse micromatch and prune ignored directories so node_modules is never crawled.

Configuration

  • UI5_WATCH_MODE=polling|native forces the file watcher backend.

Documented under Troubleshooting.

Fixes: #1479

@RandomByte
RandomByte force-pushed the fix/fs-watch-in-containers branch 6 times, most recently from 8b901c7 to 00ad237 Compare August 12, 2026 16:15
@RandomByte RandomByte changed the title fix(project): Add polling fallback for file watching in containers fix(project): Add polling file watcher for containers Aug 12, 2026
@RandomByte
RandomByte force-pushed the fix/fs-watch-in-containers branch 4 times, most recently from e43cbec to 209649c Compare August 13, 2026 14:27
On a bind-mounted volume inside a container, Linux inotify reports the
container's own writes but not writes made to the same volume from
outside the container. Podman is the common case. The incremental build
derives "what changed" solely from @parcel/watcher events, so those
missed events break rebuilds and live reload with no error to catch.

Add a fileWatcher.js module that all three watcher consumers call instead
of @parcel/watcher directly. It selects a backend once per process and
exposes a subscribe() matching @parcel/watcher's exact contract, so a
caller cannot tell which backend is active. The native backend stays
@parcel/watcher.

Both backends are imported lazily, only when selected. @parcel/watcher
resolves a native binding at load time and throws when the platform's
prebuilt binary is not installed (for example on an unsupported platform
or after an install with --omit=optional), so a static import would break
every consumer before the polling fallback could run. Loading it on demand
contains that failure: when the native backend is selected but cannot
load, subscribe() logs a warning and falls back to polling, which needs no
native code.

Parcel offers no polling subscription (its brute-force backend is
query-only), so the polling backend lives in a separate pollingWatcher.js
that fileWatcher.js imports lazily. It walks the tree and diffs an
mtimeMs+size snapshot every 250 ms (rescheduling itself after each walk so
a slow crawl cannot overlap the next), emitting the same {type, path}
events the native backend would. Errors flow through the callback so each
consumer's existing recovery path fires unchanged. The 250 ms interval
stays below WATCHER_BURST_SETTLE_MS (550 ms) so downstream event batching
still holds. Ignore globs reuse micromatch (already a direct dependency)
and prune ignored directories so node_modules is never crawled.

The backend is chosen once per process and memoized. UI5_WATCH_MODE
forces it (polling or native); otherwise polling is the default inside a
container and the native backend elsewhere. A container is detected from
the runtime marker files (/.dockerenv, /run/.containerenv) and PID 1's
cgroup. Detection is a heuristic: it can miss an exotic sandbox or match
a host whose PID 1 cgroup carries those names, so UI5_WATCH_MODE is the
escape hatch either way, documented under Troubleshooting.

WatchHandler, ProjectDefinitionWatcher, and projectGraphSettleWatcher
swap their parcelWatcher.subscribe call for fileWatcher.js; callback
bodies, ignore globs, and settle windows are untouched.

Fixes: #1479
@RandomByte
RandomByte force-pushed the fix/fs-watch-in-containers branch from 209649c to a2178cd Compare August 13, 2026 14:52
@RandomByte
RandomByte requested a review from matz3 August 13, 2026 15:08
@RandomByte

Copy link
Copy Markdown
Member Author

@matz3 kindly re-test in podman. Hopefully the new environment detection will choose the polling backend automatically now.

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.

UI5 Build Cache does not work in case process is executed in a container

1 participant