Skip to content

feat: add file watching capabilities with inotify support#324

Open
whoiskatrin wants to merge 23 commits intomainfrom
feature/file-watching
Open

feat: add file watching capabilities with inotify support#324
whoiskatrin wants to merge 23 commits intomainfrom
feature/file-watching

Conversation

@whoiskatrin
Copy link
Collaborator

Adds real-time file watching using Linux's native inotify via inotifywait.

API


// High-level 
const watcher = await sandbox.watch('/app/src', {
  recursive: true,
  include: ['*.ts', '*.js'],
  onEvent: (event) => console.log(event.type, event.path),
});
await watcher.stop();
// Low-level 
const stream = await sandbox.watchStream(path, options);
await sandbox.stopWatch(watchId);
await sandbox.listWatches();

Implementation

  • WatchService in container spawns inotifywait and streams SSE events
  • FileWatch class uses state machine pattern for lifecycle management
  • Default excludes: .git, node_modules, .DS_Store

@changeset-bot
Copy link

changeset-bot bot commented Jan 2, 2026

🦋 Changeset detected

Latest commit: a2e6bcc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/sandbox Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…ing or encoding

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Jan 2, 2026
Documents the new sandbox.watch() method that enables real-time filesystem
monitoring using Linux inotify. Includes:

- API reference for watch() method in files.mdx
- Comprehensive how-to guide with practical examples
- Cross-references between related documentation

This documentation covers:
- Basic file watching with callbacks
- Filtering by file type with glob patterns
- Event type filtering (create, modify, delete, rename)
- Cancellation with AbortController
- Hot reload and log monitoring examples
- Best practices and troubleshooting

Synced from cloudflare/sandbox-sdk#324
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/sandbox-sdk/@cloudflare/sandbox@324

commit: a2e6bcc

@github-actions
Copy link
Contributor

github-actions bot commented Jan 2, 2026

🐳 Docker Images Published

Default:

FROM cloudflare/sandbox:0.0.0-pr-324-95f82e2

With Python:

FROM cloudflare/sandbox:0.0.0-pr-324-95f82e2-python

With OpenCode:

FROM cloudflare/sandbox:0.0.0-pr-324-95f82e2-opencode

Version: 0.0.0-pr-324-95f82e2

Use the -python variant if you need Python code execution, or -opencode for the variant with OpenCode AI coding agent pre-installed.


📦 Standalone Binary

For arbitrary Dockerfiles:

COPY --from=cloudflare/sandbox:0.0.0-pr-324-95f82e2 /container-server/sandbox /sandbox
ENTRYPOINT ["/sandbox"]

Download via GitHub CLI:

gh run download 21896848909 -n sandbox-binary

Extract from Docker:

docker run --rm cloudflare/sandbox:0.0.0-pr-324-95f82e2 cat /container-server/sandbox > sandbox && chmod +x sandbox

claude[bot]

This comment was marked as outdated.

claude[bot]

This comment was marked as outdated.

claude[bot]

This comment was marked as outdated.

@whoiskatrin whoiskatrin marked this pull request as ready for review January 2, 2026 19:55
@whoiskatrin whoiskatrin marked this pull request as draft January 2, 2026 20:57
…ses. Update tests to validate new event parsing logic and ensure proper handling of inotifywait output.
claude[bot]

This comment was marked as outdated.

claude[bot]

This comment was marked as outdated.

Resolve conflicts in container.ts and sandbox.ts:
- Keep WatchService import in container.ts
- Keep watch methods and merge updated exposePort with token parameter
github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

Streaming responses (like file watch events) were blocking the WebSocket
message handler because handleStreamingResponse was awaited. This prevented
other messages from being processed while a stream was active.

Run streaming response handlers in the background with error logging,
allowing the message handler to return immediately and process subsequent
messages.
github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

If inotifywait fails to output 'Watches established' within 10 seconds,
the function will return and allow the stream to proceed. This prevents
indefinite hangs if inotifywait behaves unexpectedly.
github-actions[bot]

This comment was marked as duplicate.

For WebSocket streaming, errors were deferred until stream consumption.
This caused issues where watchStream() would return successfully even
when the server returned an error response.

Now requestStream() waits for the first message before returning:
- If it's a stream chunk, return the stream (success case)
- If it's an error response, throw immediately (error case)

This makes WebSocket streaming behavior match HTTP streaming, where
errors are thrown immediately rather than deferred.
github-actions[bot]

This comment was marked as outdated.

@whoiskatrin whoiskatrin marked this pull request as ready for review January 15, 2026 08:09
@parssak
Copy link

parssak commented Feb 7, 2026

hey! I noticed there are docs for this but doesn't seem like the changes are live yet

would love to use this API, any timelines on when this'll get merged?

@whoiskatrin
Copy link
Collaborator Author

/bonk resolve conflict and review this PR

Copy link

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge Conflict Resolved + Code Review

Conflict Resolution

Resolved the merge conflict in packages/sandbox-container/src/core/container.ts. The conflict was between the feature branch's watchHandler and main's ptyWsHandler — both are needed, so I included both in the Dependencies interface, the initialize() method, and the dependency registration block.

Build/typecheck/lint/unit tests all pass after the merge.


Code Review

Overall this is a well-architected feature that follows the three-layer SDK pattern correctly. The SSE streaming approach, DI integration, client pattern, and type safety are all solid. There are however several issues worth addressing, roughly in order of priority.

Issues to Address Before Merge

1. Empty catch blocks in cleanup paths (multiple locations)

Several catch blocks silently swallow errors that could indicate real problems:

  • watch-service.ts:390-393proc.kill() in the finally block catches everything with just a comment "Process may already be dead". This hides permission errors (EPERM), system failures, etc. At minimum log at debug level and distinguish ESRCH (expected) from other errors.

  • watch-service.ts:477 — Controller enqueue failure caught silently. Could lose error messages from inotifywait.

  • watch-service.ts:563 — Same pattern in continueStderrMonitoring.

  • file-watch.ts:168this.reader.cancel().catch(() => {}) in the finally block.

Per AGENTS.md: "silent failures are unacceptable." These should at minimum log at debug level.

2. FileWatch.established() can hang forever with AbortSignal

file-watch.ts:115-129 — If the watch is cancelled via AbortSignal during establishment before receiving the "watching" event, the runLoop throws and calls establishedReject, which is correct. However, if abort fires after established() is called but before the runLoop picks it up, there's a window where the promise could hang. The runLoop does check signal.aborted in the while condition, but if reader.read() is blocked waiting for data, the abort won't interrupt it until data arrives or the stream ends.

Consider adding a timeout or connecting the abort signal to reject the establishment promise directly.

3. isFileWatchSSEEvent type guard is weak

file-watch.ts:175-180 — Only validates that type is a string in the allowed set, but doesn't validate that required fields exist for each event type. For event type events, eventType, path, and isDirectory could all be undefined, leading to runtime errors or silent data loss when passed to onEvent callback.

4. Changeset description is implementation-focused

Per AGENTS.md: changesets should be user-facing with clear benefit and usage example. Current:

feat: add file watching capabilities with inotify support

Should explain benefit and show usage:

Add real-time file watching for detecting filesystem changes as they happen.

Use `sandbox.watch()` to monitor directories for create, modify, delete, and rename events:
const watcher = await sandbox.watch('/app/src', {
  recursive: true,
  onEvent: (event) => console.log(event.type, event.path),
});
await watcher.stop();

5. Debug logging left in E2E tests

file-watch-workflow.test.ts:204,257,404-408,436,451-452,485-488 — Multiple console.log('[DEBUG]...') statements should be removed before merge. Other E2E tests in the codebase don't have debug logging.

6. stopWatch uses UNKNOWN_ERROR instead of specific code

watch-service.ts:114 — When process.kill() fails in stopWatch(), it returns ErrorCode.UNKNOWN_ERROR. The codebase convention is to use specific error codes (like PROCESS_NOT_FOUND, PORT_NOT_EXPOSED). Consider adding WATCH_STOP_ERROR or reusing WATCH_START_ERROR (since it's a watch lifecycle error).

7. WatchService not integrated into server shutdown

The WatchService spawns inotifywait processes but there's no cleanup hook in packages/sandbox-container/src/server.ts. When the container shuts down, these processes could become zombies. Add watchService.stopAllWatches() to the server cleanup sequence.

Minor Items

  • watch-service.ts:131-137 — Comments reference historical context ("Expected case: process already exited"). Per AGENTS.md, describe current behavior instead of referencing what the code prevents.

  • E2E tests use fixed delays (1500ms, 3000ms) that could cause flakiness in slower CI environments. The watchWithActions helper's delays should be configurable or event-driven.

  • file-watch.ts:189-191 — Malformed JSON is silently ignored with no logging. This makes debugging "missing events" very difficult in production. Consider logging at debug level.

Strengths

  • Secure glob matching: Character-by-character regex construction with comprehensive ReDoS prevention tests is exemplary.
  • State machine design in FileWatch: Clean establishing → active → stopped transitions with proper error handling at each transition.
  • WatchService follows established patterns: DI via constructor injection, ServiceResult<T> return type, proper logger child creation.
  • Comprehensive unit tests: Good coverage of glob matching, inotify event parsing, and argument building. Uses createNoOpLogger() as documented.
  • E2E tests cover real workflows: Creation, modification, deletion, filtering, stop/list APIs, and error cases.
  • Proper error codes: WATCH_NOT_FOUND and WATCH_START_ERROR with correct HTTP status mappings.
  • WebSocket transport improvements: Partial SSE event state tracking across chunks is a correct fix for chunked event parsing.

Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>
@ask-bonk
Copy link

ask-bonk bot commented Feb 10, 2026

Done. Here's a summary of what I did:

Merge Conflict Resolution

Resolved the single conflict in packages/sandbox-container/src/core/container.ts where the feature branch added WatchHandler and main added PtyWebSocketHandler. Both were included in the merged result. The merge commit has been pushed.

Verification

  • Biome lint: clean (295 files checked, no issues)
  • TypeScript typecheck: all 14 packages pass
  • Unit tests: all pass across all packages

Review Summary

The PR adds a well-architected file watching feature using inotify that follows the three-layer SDK pattern correctly. Key strengths include secure glob matching with ReDoS prevention, a clean state machine in FileWatch, and comprehensive test coverage.

7 issues identified ranging from silent error swallowing in cleanup paths, a potential hang in FileWatch.established() with AbortSignal, a weak type guard, implementation-focused changeset description, debug logging left in E2E tests, a generic error code where a specific one should be used, and missing server shutdown cleanup for watch processes. The full review is posted on the PR.

github run

github-actions[bot]

This comment was marked as outdated.

@whoiskatrin
Copy link
Collaborator Author

hey! I noticed there are docs for this but doesn't seem like the changes are live yet

would love to use this API, any timelines on when this'll get merged?

really keen to get this in this week or next week. sorry about the docs, someone got them merged ahead of time

github-actions[bot]

This comment was marked as outdated.

@whoiskatrin
Copy link
Collaborator Author

/bonk fix these code review issues that you listed

Replace empty catch blocks with debug/warn logging throughout
WatchService and FileWatch to make failures visible. Fix
FileWatch.established() to reject on AbortSignal during
establishment, preventing indefinite hangs. Strengthen the SSE
event type guard to validate required fields per event type.
Add WATCH_STOP_ERROR code, integrate watch cleanup into server
shutdown, and rewrite changeset for end users.
@ask-bonk
Copy link

ask-bonk bot commented Feb 10, 2026

All 7 review issues have been fixed. Here's a summary of what was changed:

Changes Made

1. Empty catch blocks in watch-service.ts (4 locations)

  • proc.kill() in finally block: Now logs at warn level and distinguishes ESRCH (expected - process already dead) from real errors
  • controller.enqueue() in waitForWatchesEstablished: Now logs at debug level when controller is closed
  • reader.releaseLock(): Now logs at debug level on failure
  • controller.enqueue() in continueStderrMonitoring: Now logs at debug level when stream is closed

2. FileWatch.established() hang with AbortSignal (file-watch.ts)

  • Added abort signal listener that rejects the establishment promise directly if abort fires during establishment
  • Properly cleans up abort listener when establishment resolves/rejects normally

3. Weak isFileWatchSSEEvent type guard (file-watch.ts)

  • Now validates required fields per event type: watching requires path + watchId, event requires eventType + path + isDirectory, error requires error, stopped requires reason

4. Changeset description (.changeset/empty-poets-serve.md)

  • Rewritten to be user-focused with clear benefit description and usage example

5. Debug logging in E2E tests (file-watch-workflow.test.ts)

  • Removed all 8 console.log('[DEBUG]...') statements

6. UNKNOWN_ERROR in stopWatch (watch-service.ts, codes.ts, status-map.ts)

  • Added WATCH_STOP_ERROR error code with 500 status mapping
  • Updated stopWatch() to use the specific code instead of UNKNOWN_ERROR

7. Missing server shutdown cleanup (server.ts)

  • Added watchService.stopAllWatches() to the server cleanup sequence, running before process/port cleanup

github run

github-actions[bot]

This comment was marked as outdated.

@whoiskatrin
Copy link
Collaborator Author

/bonk investigate tests failures and fix if needed

@cloudflare cloudflare deleted a comment from ask-bonk bot Feb 10, 2026
@ask-bonk
Copy link

ask-bonk bot commented Feb 10, 2026

Bonk workflow was cancelled.

View workflow run

The watchWithActions timeout started at stream creation but blocked
for ~6.5s inside the event handler (pre-action delay + file ops +
post-action delay), leaving insufficient time to read events in CI.
Reset the timeout after actions complete so the full window is
available for event collection.
github-actions[bot]

This comment was marked as outdated.

The combined wait time (~18s) approached the 30s Vitest timeout.
With excludes filtering most events, only 2-4 arrive, so the high
stopAfterEvents threshold was never reached and the test always
fell through to the full 12s reader timeout.
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

This PR implements a comprehensive file watching feature using inotify, adding significant value for development tools and automation workflows. The implementation follows the project's three-layer architecture properly and includes extensive testing.

Strengths

Solid architecture: The three-layer design is well-executed:

  • SDK layer (FileWatch class) provides high-level lifecycle management
  • Container layer (WatchService) handles inotify process spawning and SSE streaming
  • Proper separation of concerns with modular client pattern

Robust implementation: Key quality aspects are well-handled:

  • State machine pattern in FileWatch prevents race conditions during establishment
  • Comprehensive error handling and cleanup (process killing, stream cancellation)
  • Security-conscious glob matching prevents ReDoS attacks
  • Extensive unit tests with private method testing via type assertions

Performance optimizations: Server-side filtering with inotify --exclude prevents unnecessary events from reaching the JavaScript layer, which is much more efficient than client-side filtering.

Technical Considerations

WebSocket streaming complexity: The deferred stream return logic in ws-transport.ts adds significant complexity to handle first-message waiting. While this may be necessary for the WebSocket transport, the promise-within-stream pattern makes error handling intricate. Worth reviewing if there's a simpler approach.

Stream lifecycle management: The activeStreams Map and reader acquisition timing changes in ws-adapter.ts address Bun GC issues, but this fix should be verified to ensure it actually prevents the original problem.

Container shutdown: Watch cleanup during shutdown could potentially hang if inotify processes are unresponsive. Consider adding timeouts to prevent blocking container shutdown.

Path resolution: The relative path handling in watch-handler.ts is functional but could be more robust for edge cases like ./ prefixes or .. components.

Documentation Alignment

The implementation aligns well with the Cloudflare documentation. The API surface matches the documented interfaces and the feature set is complete.

Testing Coverage

Excellent E2E test coverage covers the major workflows. The unit tests for WatchService are comprehensive and include security testing for glob patterns.

Verdict

This is a well-implemented feature that adds significant value. The technical considerations noted above are worth reviewing but don't block the core functionality. The architecture follows project patterns and the testing is thorough.

Looks good to merge - the file watching capability is solid and will be valuable for development tools and automation workflows.

@parssak
Copy link

parssak commented Feb 17, 2026

@whoiskatrin any rough estimates as to when this'll be in the next release? 🙏🏻

@whoiskatrin
Copy link
Collaborator Author

@parssak this will get in this week

@parssak
Copy link

parssak commented Feb 18, 2026

That's great to hear! Tysm

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.

2 participants

Comments