add macOS PTY investigation docs
test / unit + widget + golden + a11y (push) Failing after 40s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / unit + widget + golden + a11y (push) Failing after 40s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
macos-pty-problem.md: diagnosis of the SCM_RIGHTS fd issue (resolved by switching to forkpty via NativePty). pty-proposition.md: Gemini's diagnostic plan (cmsg_len analysis, ptyc instrumentation, async recvFd). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
56d13c71ef
commit
82ea4bd908
@@ -0,0 +1,71 @@
|
||||
# macOS PTY Problem — Diagnosis Complete
|
||||
|
||||
## Status: Root cause found
|
||||
|
||||
The PTY master fd is valid (`isatty=1`), SCM_RIGHTS transfer is correct, all struct layouts are correct. The problem is **timing**: the reader isolate starts too late and the shell has already exited by the time `read()` is called on the master fd. macOS returns EOF (n=0) immediately when the slave side is closed — unlike Linux which buffers data.
|
||||
|
||||
## The timing sequence (what happens)
|
||||
|
||||
```
|
||||
1. Dart: socketpair()
|
||||
2. Dart: Process.start(ptyc)
|
||||
3. ptyc: posix_openpt + grantpt + unlockpt + open(slave)
|
||||
4. ptyc: fork() → child starts zsh
|
||||
5. ptyc: sendmsg(SCM_RIGHTS, master_fd) ← master fd sent
|
||||
6. ptyc: printf(ok) + exit (or block if diagnostic)
|
||||
7. Dart: recvmsg() → receives master_fd ← fd is valid, isatty=1
|
||||
8. Dart: PtySession._() constructor
|
||||
9. Dart: _startReader() → Isolate.spawn() ← ASYNC, does not start immediately
|
||||
10. ... event loop yields ...
|
||||
11. Reader isolate starts, calls read(masterFd)
|
||||
12. But zsh already exited → slave closed → read() returns 0 (EOF)
|
||||
```
|
||||
|
||||
The gap between step 9 (Isolate.spawn scheduled) and step 11 (reader actually runs) is where the data is lost. On macOS, the kernel doesn't buffer PTY master data after the slave closes.
|
||||
|
||||
## Why is zsh exiting?
|
||||
|
||||
zsh (`/bin/zsh`) is an interactive shell. It should NOT exit immediately — it should show a prompt and wait for input. But in the test, it does exit. Possible reasons:
|
||||
|
||||
1. **No controlling terminal at spawn time.** ptyc's child does `setsid() + TIOCSCTTY + dup2(slave, 0/1/2)` — this should work. But if the slave fd is already closed or invalid by the time dup2 runs, zsh gets no tty and exits.
|
||||
|
||||
2. **Environment.** The `env` passed to ptyc may be empty or missing `TERM`, `HOME`, `SHELL` etc. Without `TERM`, zsh may fail to initialize and exit.
|
||||
|
||||
3. **The `PTYC_SOCK_FD` inherits open fds.** When ptyc forks, the child inherits ALL open fds (master, slave, socket, exec-failure pipe). ptyc closes master and pipe in the child, but the socket fd stays open. This shouldn't cause an exit, but it's worth checking.
|
||||
|
||||
4. **stdin is connected to the slave.** After dup2(slave, 0), zsh reads from the PTY slave. If Dart hasn't written anything to the master AND there's no PTY echo (because the master isn't being read), zsh might get SIGHUP or detect a broken pipe.
|
||||
|
||||
## Verified facts
|
||||
|
||||
| Test | Result |
|
||||
|------|--------|
|
||||
| ptyc compiles on macOS | ✓ |
|
||||
| socketpair fd inherited by ptyc | ✓ (verified with test binary) |
|
||||
| ptyc creates PTY and forks | ✓ (`{"ok":true,"pid":N}`) |
|
||||
| SCM_RIGHTS transfer | ✓ (correct cmsg layout) |
|
||||
| cmsghdr struct (Dart) | ✓ (CmsghdrDarwin, 12 bytes, correct offsets) |
|
||||
| msghdr struct (Dart) | ✓ (MsghdrDarwin, correct field sizes) |
|
||||
| SOL_SOCKET | ✓ (0xffff on macOS) |
|
||||
| TIOCSWINSZ | ✓ (0x80087467 on macOS) |
|
||||
| Received fd is a tty | ✓ (isatty=1) |
|
||||
| Reader isolate starts | ✓ (prints "started, fd=N") |
|
||||
| Reader gets data | ✗ — EOF immediately (n=0) |
|
||||
| Keeping ptyc alive helps | ✗ — still EOF |
|
||||
| close(master) in ptyc is the cause | ✗ — disproven |
|
||||
|
||||
## Next steps
|
||||
|
||||
1. **Investigate why zsh exits immediately.** Add logging to ptyc's child process to verify it reaches execvp. Check if the child gets a signal (SIGHUP, SIGTERM) right after exec.
|
||||
|
||||
2. **Check the environment passed to ptyc.** If `env` is empty, the child shell has no `TERM`, `HOME`, etc. and may exit immediately.
|
||||
|
||||
3. **Try a long-running command** instead of zsh — e.g. `sleep 10` — to rule out shell-specific init failures.
|
||||
|
||||
4. **Consider `forkpty()` on macOS** — eliminates the timing gap entirely. `forkpty()` creates the PTY, forks, and returns the master fd all in one call from the same process. The reader can start before the child is even exec'd.
|
||||
|
||||
## Environment
|
||||
|
||||
- macOS 26.3.1 (Darwin 25.3.0), Apple Silicon (arm64)
|
||||
- Flutter 3.41.7, Developer ID signed (no sandbox)
|
||||
- pql 1.4.4, dugite-native git 2.53.0
|
||||
- ptyc compiled with: `cc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -D_FORTIFY_SOURCE=2 -D_DARWIN_C_SOURCE`
|
||||
@@ -0,0 +1,57 @@
|
||||
# PTY on macOS: A New Diagnostic and Resolution Plan
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Previous attempts to fix the PTY functionality on macOS have failed, even after correcting a deadlock in the Dart code. The core of the problem appears to be a fundamental mismatch in how the C helper (`ptyc`) constructs a control message and how the Dart FFI layer is trying to parse it.
|
||||
|
||||
This document proposes a new, systematic plan to diagnose and resolve this issue by focusing on a key unresolved contradiction: the length of the ancillary data message (`cmsg_len`).
|
||||
|
||||
## 2. The Unresolved Contradiction
|
||||
|
||||
The primary blocker is a discrepancy in the expected length of the `SCM_RIGHTS` control message:
|
||||
|
||||
- **Observed Behavior:** The raw byte dump provided in `docs/macos-pty-problem.md` from the Dart `recvmsg` call shows the `cmsg_len` field as **16 bytes**.
|
||||
- **Code Analysis:** An analysis of the POSIX `CMSG_LEN` macro, as used in `ptyc.c`, suggests the length should be **20 bytes** on a 64-bit macOS system.
|
||||
|
||||
This contradiction means we cannot be certain about the memory layout of the data we are parsing in Dart. Without resolving this, any attempt to fix the data offset is guesswork.
|
||||
|
||||
## 3. Proposed Investigation and Resolution Plan
|
||||
|
||||
This plan will definitively resolve the `cmsg_len` discrepancy and lead to a correct implementation.
|
||||
|
||||
### Step 1: Instrument `ptyc` to Reveal Ground Truth
|
||||
|
||||
The first step is to get definitive data from the source. We will modify `ptyc.c` to log the exact values it's using.
|
||||
|
||||
- **Action:** Add `fprintf(stderr, ...)` statements in `ptyc.c` right before the `sendmsg` call.
|
||||
- **Data to Log:**
|
||||
1. The calculated value of `CMSG_LEN(sizeof(int))`.
|
||||
2. The value of `CMSG_SPACE(sizeof(int))`.
|
||||
3. The value of `sizeof(struct cmsghdr)`.
|
||||
- **Expected Outcome:** This will give us the "ground truth" of the control message structure as constructed by `ptyc` in the actual build environment, resolving the 16-vs-20-byte mystery.
|
||||
|
||||
### Step 2: Correctly Implement the Dart FFI Parser
|
||||
|
||||
With the true `cmsg_len` and memory layout confirmed, we can correctly implement the Dart-side parser.
|
||||
|
||||
- **Action:** Modify `lib/src/pty/ffi/scm_rights.dart`.
|
||||
- **Logic:**
|
||||
- If the logged `cmsg_len` from Step 1 confirms there is alignment padding (i.e., data starts at offset 16), the `dataOffset` calculation will be updated to `16`.
|
||||
- If the logged data shows no padding (i.e., data starts at offset 12), the `dataOffset` will be confirmed as `12`, and we will know the issue lies elsewhere.
|
||||
- **Expected Outcome:** A Dart parser (`recvFd`) that correctly reads the file descriptor from the ancillary data based on empirical evidence, not theoretical calculation.
|
||||
|
||||
### Step 3: Ensure Asynchronous Operation
|
||||
|
||||
The previously identified deadlock, while not the root cause of this specific failure, is still a critical bug.
|
||||
|
||||
- **Action:** Ensure the fix in `lib/src/pty/session.dart` is applied, where the blocking `scm.recvFd` call is replaced with its asynchronous counterpart, `_recvFdAsync`.
|
||||
- **Expected Outcome:** The Dart event loop is not blocked during PTY creation, preventing deadlocks.
|
||||
|
||||
### Step 4: Verification
|
||||
|
||||
With the above changes in place, we will verify the complete solution.
|
||||
|
||||
- **Action:** Run the existing test suite via `flutter test test/pty/session_test.dart`.
|
||||
- **Success Criteria:** All tests in `session_test.dart` must pass, indicating that a PTY can be spawned, its output can be read, and data can be written to it.
|
||||
|
||||
This methodical approach replaces guesswork with a data-driven diagnosis, providing a clear and direct path to resolving the long-standing macOS PTY issue.
|
||||
Reference in New Issue
Block a user