[PR #1194] [MERGED] fix(process): stop cancelled Swift helper builds before removing their files #2707

Closed
opened 2026-10-17 21:09:19 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1194
Author: @kaoru0822-kitauji
Created: 10/9/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: fix/darwin-helper-compile-tmpdir


📝 Commits (1)

  • c717204 fix(process): stop cancelled Swift helper builds before removing their files

📊 Changes

5 files changed (+231 additions, -10 deletions)

View changed files

📝 src/native/NativeUiHelperRuntime.ts (+7 -1)
📝 src/process/DarwinProcessRunTokenReader.ts (+17 -2)
📝 src/process/ExecFileOutput.test.ts (+62 -0)
📝 src/process/ExecFileOutput.ts (+56 -7)
📝 tests/boundary/process/darwinProcessRunTokenReader.test.ts (+89 -0)

📄 Description

Summary

Cancelling the macOS process-ownership helper build left swiftc object files (TemporaryDirectory.*/ProcessRunTokenReader-1.o, …Main-1.o) in TMPDIR, even though cancellation reported cleanup: complete.

Fixes #1173

Cause (observed on macOS arm64, Xcode CLT swiftc)

Three behaviors combine:

  1. Node execFile abort: it sends SIGTERM even when killSignal is set (killSignal reaches only the timeout path), and it calls back immediately on abort, before the child exits. In a probe the callback ran 4 ms after abort() and the child exited 412 ms later. So REA removed its root while the compiler was still running.
  2. swift-driver job groups: xcrun execs swift-driver, which starts each swift-frontend in its own process group (pgid == pid). Killing the driver or its process group does not reach the jobs.
  3. Signal handling: on SIGTERM the driver exits while a job keeps writing its object under TMPDIR. On SIGINT the driver stops its jobs and deletes their objects before exiting, leaving only an empty TemporaryDirectory.*/.keep-directory. Shell experiment: SIGTERM at 0.1 s or 0.2 s → object left; SIGINT at 0.08 s through 0.35 s → no frontend alive when the driver exits, and nothing recreated after removal.

Change

  • src/process/ExecFileOutput.ts: new opt-in stopSignal option. On abort or timeout the process receives that signal, the promise rejects with an AbortError (or the timeout error) only after the process exits, and a process still running after a 5 s grace period is SIGKILLed. Callers that don't pass stopSignal keep Node's behavior.
  • src/process/DarwinProcessRunTokenReader.ts and src/native/NativeUiHelperRuntime.ts: Swift helper builds use stopSignal: "SIGINT" and set TMPDIR to a tmp/ directory inside the helper root that REA already removes. The token reader still uses Swift's default module cache; build time is unchanged (0.38 s vs 0.37 s).

Real swiftc check (built branch)

Prepared the reader under a fresh TMPDIR, aborted after 60–350 ms, closed it, waited 1.5 s, and listed TMPDIR:

  • main: every run left TemporaryDirectory.*/*.o.
  • With only the TMPDIR redirect: objects reappeared inside the already-removed helper root.
  • This branch: empty TMPDIR at all seven delays.

Tests

  • src/process/ExecFileOutput.test.ts: an aborted command receives stopSignal (the child records SIGINT after a 200 ms cleanup delay) and the rejection is observed only after that record exists; an already aborted signal rejects without starting the process. The first fails on main.
  • tests/boundary/process/darwinProcessRunTokenReader.test.ts: a fake xcrun behaves like swift-driver (a separate-process-group job writes its object 300 ms later; SIGINT stops the job and deletes the object; SIGTERM leaves it running). After cancellation and close(), the helper root does not reappear. Fails on main.

Verification

  • npm run check:fast, npm run jscpd
  • npm run test:focused -- all src/process, src/native, and tests/boundary/process tests plus every test using execFileOutput (49 files: 301 passed, 3 skipped). One failure, classifies a real hdiutil verify refusal…, also fails on main on this machine (local hdiutil behavior) and is unrelated.

The native UI runtime has no injectable compiler, so its change (the same stopSignal and TMPDIR settings) is covered by the shared tests and code review rather than its own test.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1194 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/9/2026 **Status:** ✅ Merged **Merged:** 10/9/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `fix/darwin-helper-compile-tmpdir` --- ### 📝 Commits (1) - [`c717204`](https://github.com/morluto/rea/commit/c7172047951e7dbb9df58b57ff1b43ac9bd47909) fix(process): stop cancelled Swift helper builds before removing their files ### 📊 Changes **5 files changed** (+231 additions, -10 deletions) <details> <summary>View changed files</summary> 📝 `src/native/NativeUiHelperRuntime.ts` (+7 -1) 📝 `src/process/DarwinProcessRunTokenReader.ts` (+17 -2) 📝 `src/process/ExecFileOutput.test.ts` (+62 -0) 📝 `src/process/ExecFileOutput.ts` (+56 -7) 📝 `tests/boundary/process/darwinProcessRunTokenReader.test.ts` (+89 -0) </details> ### 📄 Description ## Summary Cancelling the macOS process-ownership helper build left swiftc object files (`TemporaryDirectory.*/ProcessRunTokenReader-1.o`, `…Main-1.o`) in `TMPDIR`, even though cancellation reported `cleanup: complete`. Fixes #1173 ## Cause (observed on macOS arm64, Xcode CLT swiftc) Three behaviors combine: 1. **Node `execFile` abort**: it sends SIGTERM even when `killSignal` is set (`killSignal` reaches only the timeout path), and it calls back immediately on abort, before the child exits. In a probe the callback ran 4 ms after `abort()` and the child exited 412 ms later. So REA removed its root while the compiler was still running. 2. **swift-driver job groups**: `xcrun` execs `swift-driver`, which starts each `swift-frontend` in its own process group (`pgid == pid`). Killing the driver or its process group does not reach the jobs. 3. **Signal handling**: on SIGTERM the driver exits while a job keeps writing its object under `TMPDIR`. On SIGINT the driver stops its jobs and deletes their objects before exiting, leaving only an empty `TemporaryDirectory.*/.keep-directory`. Shell experiment: SIGTERM at 0.1 s or 0.2 s → object left; SIGINT at 0.08 s through 0.35 s → no frontend alive when the driver exits, and nothing recreated after removal. ## Change - `src/process/ExecFileOutput.ts`: new opt-in `stopSignal` option. On abort or timeout the process receives that signal, the promise rejects with an `AbortError` (or the timeout error) only after the process exits, and a process still running after a 5 s grace period is SIGKILLed. Callers that don't pass `stopSignal` keep Node's behavior. - `src/process/DarwinProcessRunTokenReader.ts` and `src/native/NativeUiHelperRuntime.ts`: Swift helper builds use `stopSignal: "SIGINT"` and set `TMPDIR` to a `tmp/` directory inside the helper root that REA already removes. The token reader still uses Swift's default module cache; build time is unchanged (0.38 s vs 0.37 s). ## Real swiftc check (built branch) Prepared the reader under a fresh `TMPDIR`, aborted after 60–350 ms, closed it, waited 1.5 s, and listed `TMPDIR`: - `main`: every run left `TemporaryDirectory.*/*.o`. - With only the `TMPDIR` redirect: objects reappeared inside the already-removed helper root. - This branch: empty `TMPDIR` at all seven delays. ## Tests - `src/process/ExecFileOutput.test.ts`: an aborted command receives `stopSignal` (the child records `SIGINT` after a 200 ms cleanup delay) and the rejection is observed only after that record exists; an already aborted signal rejects without starting the process. The first fails on `main`. - `tests/boundary/process/darwinProcessRunTokenReader.test.ts`: a fake `xcrun` behaves like swift-driver (a separate-process-group job writes its object 300 ms later; SIGINT stops the job and deletes the object; SIGTERM leaves it running). After cancellation and `close()`, the helper root does not reappear. Fails on `main`. ## Verification - `npm run check:fast`, `npm run jscpd` - `npm run test:focused --` all `src/process`, `src/native`, and `tests/boundary/process` tests plus every test using `execFileOutput` (49 files: 301 passed, 3 skipped). One failure, `classifies a real hdiutil verify refusal…`, also fails on `main` on this machine (local `hdiutil` behavior) and is unrelated. The native UI runtime has no injectable compiler, so its change (the same `stopSignal` and `TMPDIR` settings) is covered by the shared tests and code review rather than its own test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:09:19 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2707
No description provided.