Skip to content

add ptsname_r shim for macos#4

Merged
ethanpailes merged 10 commits intoshell-pool:masterfrom
seruman:ptsname_r-macos
Jan 7, 2026
Merged

add ptsname_r shim for macos#4
ethanpailes merged 10 commits intoshell-pool:masterfrom
seruman:ptsname_r-macos

Conversation

@seruman
Copy link
Contributor

@seruman seruman commented Nov 3, 2025

Related to; shell-pool/shpool#183

I was not able to build shell-pool/shpool;

error[E0425]: cannot find function `ptsname_r` in crate `libc`
    --> src/fork/pty/master/mod.rs:73:29
     |
73   |                 match libc::ptsname_r(fd, data as *mut libc::c_char, buf.len()) {
     |                             ^^^^^^^^^ help: a function with a similar name exists: `ptsname`

With a quick search I found these references:

This PR adds a small macOS-only shim so that from the caller’s perspective, it still looks like ibc::ptsname_r, to make as little as change possible.

I have little to no experience in Rust or systems programming, so this change
may not be idiomatic or does not even make sense in the first place so feel
free to update/feedback or even discard.

I've been using it on on my shpool fork for couple some time just fine.

Disclaimer; got help from LLM tools, especially for tests.


Had to change tests to use sh instead of bash for portability as on my version of macOS -26.0.1-, Apple provided bash greets you with;

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

And seems like it is baked in.

@google-cla
Copy link

google-cla bot commented Nov 3, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Contributor

@ethanpailes ethanpailes left a comment

Choose a reason for hiding this comment

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

Thank you so much for this patch!

Overall, it looks great, I mostly just have feedback about getting the change to follow project standards when it comes to saftey.

if let Some(fd) = self.pty {
// Safety: the vector's memory is valid for the duration
// of the call
unsafe {
Copy link
Contributor

@ethanpailes ethanpailes Nov 3, 2025

Choose a reason for hiding this comment

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

I'd like to maintain the style that every unsafe block has a saftey comment above it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mistakenly moved this one, updated; 559aa75

//! Based on: https://tarq.net/posts/ptsname-on-osx-with-rust/

#[cfg(any(target_os = "macos"))]
pub unsafe fn ptsname_r(
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a "Saftey" comment that explains the formal precondtions that any call site must meet in order to use this function correctly (should be pretty easy, just stuff like "buf needs to point to allocated memory" and "buflen can't be longer than the allocation" and "fd must be an open file descriptor").

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added; 559aa75

return *libc::__error();
}

let mut len = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Totally fine to leave it like this, but for this sort of null-seaking, memchr (either the pure rust crate https://crates.io/crates/memchr, or the libc function https://docs.rs/libc/0.2.177/libc/fn.memchr.html) will do this a lot faster because they take advantage of SIMD hardware acceleration. It definitely won't matter for performance since the strings are so small here, so this is mostly just me being nerd sniped.

Copy link
Contributor Author

@seruman seruman Nov 7, 2025

Choose a reason for hiding this comment

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

TIL thank you. To not bring a new dependency, went for the libc one; 3bca717

return libc::ERANGE;
}

libc::memcpy(
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use std::ptr::copy (which is how you pronounce libc::memmove in rust) here. It is safer because it can handle overlapping ranges, and on modern branch predicting CPUs the extra branch it requires is undetectable in microbenchmarks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated; 3bca717

let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };
assert!(master_fd >= 0, "Failed to open master PTY");

unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a Saftey comment. I know it's just a test, but having them on every block makes auditing easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated each unsafe section in the tests, not sure about the wording though. How all these sounds like? 559aa75


let mut buf = [0u8; 2];
let result =
unsafe { ptsname_r(master_fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
Copy link
Contributor

Choose a reason for hiding this comment

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

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#[test]
fn test_ptsname_r_invalid_fd() {
let mut buf = vec![0u8; 1024];
let result = unsafe { ptsname_r(-1, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
Copy link
Contributor

Choose a reason for hiding this comment

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

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#[cfg(any(target_os = "macos"))]
#[test]
fn test_ptsname_r_null_buffer() {
let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };
Copy link
Contributor

Choose a reason for hiding this comment

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

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) };

if master_fd >= 0 {
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

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

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let result = unsafe { ptsname_r(master_fd, std::ptr::null_mut(), 1024) };

unsafe {
libc::close(master_fd);
Copy link
Contributor

Choose a reason for hiding this comment

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

Saftey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ethanpailes
Copy link
Contributor

Looks great. Thanks! Looks like there is a bit of lint bitrot (https://github.com/shell-pool/shpool_pty/actions/runs/19173539720/job/54819707170?pr=4) that needs fixing and then we can merge.

@ethanpailes
Copy link
Contributor

If you don't have access to that lint output for some reason, you should be able to repro it by running cargo +nightly cranky --all-targets -- -D warnings

@seruman
Copy link
Contributor Author

seruman commented Nov 7, 2025

If you don't have access to that lint output for some reason, you should be able to repro it by running cargo +nightly cranky --all-targets -- -D warnings

They all should be fixed now.

@ethanpailes
Copy link
Contributor

Not sure why that test is spinning. At this point is seems like it is going to time out.

@seruman
Copy link
Contributor Author

seruman commented Nov 7, 2025

Not sure why that test is spinning. At this point is seems like it is going to time out.

I think I broke something. On macOS tests pass but when I try to run on a Linux vm it just hangs.

@seruman
Copy link
Contributor Author

seruman commented Nov 7, 2025

I think you might want to abort the run. It seems to be due to replacing bash with sh. I'm trying to understand why.

@ethanpailes
Copy link
Contributor

I'm kinda supprised it ran for over an hour without timing out.

@seruman
Copy link
Contributor Author

seruman commented Nov 7, 2025

My guess is sh is waiting input on prompt before exit somehow not sure how to assert it though, I'll see what I can. I can slap another #[cfg(target_os = "macos")] to switch only on macOS but feels dirty.

Comment on lines +31 to +39
let mut cmd = Command::new("bash");
cmd.env_clear();

// On macOS, silence the deprecation warning;
// https://github.com/apple-oss-distributions/bash/blob/e86b2aa8e37a31f8fce56366d1abaf08a3fac7d2/bash-3.2/shell.c#L760-L765
#[cfg(target_os = "macos")]
{
cmd.env("BASH_SILENCE_DEPRECATION_WARNING", "1");
}
Copy link
Contributor Author

@seruman seruman Nov 7, 2025

Choose a reason for hiding this comment

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

Looks like /bin/sh is just bash in disguise on macOS. Would it be OK to stick to bash and just silence this annoying warning?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that seems fine to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cheers

@c22
Copy link

c22 commented Jan 7, 2026

Found my way here from shell-pool/shpool#48

Looks like most if not all the feedback has been taken up. Do you guys need any help testing this?

It would be great if @seruman wouldn't have to maintain a fork for folks like me!

Comment on lines +31 to +39
let mut cmd = Command::new("bash");
cmd.env_clear();

// On macOS, silence the deprecation warning;
// https://github.com/apple-oss-distributions/bash/blob/e86b2aa8e37a31f8fce56366d1abaf08a3fac7d2/bash-3.2/shell.c#L760-L765
#[cfg(target_os = "macos")]
{
cmd.env("BASH_SILENCE_DEPRECATION_WARNING", "1");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that seems fine to me.

@ethanpailes
Copy link
Contributor

@seruman in the future, when you finish responding to a review, you can click the little recycle icon on the upper right of the PR page to notify the reviewer that you are done responding and request a re-review.

2026-01-07T08:12:1767798768

@seruman
Copy link
Contributor Author

seruman commented Jan 7, 2026

@ethanpailes thanks for the heads up, sorry that i totally forgot this PR.

@ethanpailes ethanpailes merged commit b78f49f into shell-pool:master Jan 7, 2026
5 checks passed
@seruman seruman deleted the ptsname_r-macos branch January 7, 2026 15:16
@ethanpailes
Copy link
Contributor

@ethanpailes thanks for the heads up, sorry that i totally forgot this PR.

No worries, I forgot about it too. Thank you for the patch!

I'll cut a new release so we can pick up the change in the main shpool repo.

@ethanpailes
Copy link
Contributor

I got a little too eager on merging it looks like. I posted #5 as a followup.

@ethanpailes
Copy link
Contributor

@seruman I just cut and pushed 0.3.2 to crates.io (https://crates.io/crates/shpool_pty) with this change. If you have a macos port for the main shpool repo that you want to merge in I would be happy to help you with that.

@seruman
Copy link
Contributor Author

seruman commented Jan 7, 2026

@seruman I just cut and pushed 0.3.2 to crates.io (https://crates.io/crates/shpool_pty) with this change. If you have a macos port for the main shpool repo that you want to merge in I would be happy to help you with that.

thank you, yes i do. i'm a little caught up though, it might take some time before getting to it.

@ethanpailes
Copy link
Contributor

Awesome! No rush

c22 added a commit to c22/shpool that referenced this pull request Jan 29, 2026
This version includes macOS support via ptsname (vs ptsname_r).

See: shell-pool/shpool_pty#4
c22 added a commit to c22/shpool that referenced this pull request Jan 29, 2026
This version includes macOS support via ptsname (vs ptsname_r).

See: shell-pool/shpool_pty#4
c22 added a commit to c22/shpool that referenced this pull request Feb 5, 2026
This version includes macOS support via ptsname (vs ptsname_r).

See: shell-pool/shpool_pty#4
c22 added a commit to c22/shpool that referenced this pull request Feb 5, 2026
This version includes macOS support via ptsname (vs ptsname_r).

See: shell-pool/shpool_pty#4
ethanpailes pushed a commit to shell-pool/shpool that referenced this pull request Feb 5, 2026
This patch adds support for building and running on macos. As of this change, most of the test suite passes on macos, with the exception of some shell tests that just need to be tweaked and the motd pager tests, which have deeper issues.

Full log of squashed commit messages:

* use platform agnostic wather instead of inotify

* add macos/bsd specific passwd fields

* use std::env::current_exe instead of proc fs

* add macos specific peer creds handling

possibly the same in BSD

* use getpeereid instead of LOCAL_PEERCRED socket opts

basically the same according to manual

* fix: use into_owned() to avoid double allocation

* fix: canonicalize watched paths in ConfigWatcher

File system watchers (inotify, FSEvents) report canonical paths, but ConfigWatcher was storing paths as provided by the caller. When a path contained symlinks (e.g., /var -> /private/var on macOS), event paths wouldn't match stored paths, causing config changes to be missed.

Add canonicalize_path() to resolve symlinks in the existing portion of a path (handling paths where the final components don't exist yet), and apply it when storing paths in the watcher.

Include a regression test that creates a symlink and verifies events are received when watching through the symlink.

* fix: correct test synchronization in ConfigWatcher

The worker_ready() test helper signals when the worker is idle, but it was signaling even when there was a pending reload_deadline. This could cause tests to proceed before the debounce timeout fired, leading to flaky test results.

Only signal idle when there's no pending work (reload_deadline is None).

Update the debounce test to not call worker_ready() between writes, as that now correctly waits for the reload to complete.

* chore: bump shpool_pty to 0.3.2

This version includes macOS support via ptsname (vs ptsname_r).

See: shell-pool/shpool_pty#4

* test: fix integration tests for macOS

- Forward test_hooks feature from shpool to libshpool crate
- Shorten socket paths to avoid macOS 104-byte sun_path limit
- Forward HOME and PATH env vars to attach subprocess (macOS typically lacks XDG_RUNTIME_DIR, needs HOME instead)
- Use std::env::var directly to avoid unused import warning
- Document that tests should run with --test-threads=1 on macOS due to FSEvents timing behavior under concurrent load

* fix: handle EINVAL on set_read_timeout for macOS

macOS returns EINVAL when setting socket timeout on a connection where the peer has already closed (documented in setsockopt(2)).

This typically happens with daemon presence probe connections.

* fix: handle ENOTCONN on socket shutdown for macOS

On macOS, calling shutdown() on a Unix socket after the peer has disconnected returns ENOTCONN (error 57). This was causing the shell->client thread to exit with an error, which then caused re-attachment to fail.

Add a shutdown_socket() helper that ignores ENOTCONN errors since the socket is already closed in that case.

* test: fix race condition in move_multiple_levels_in_place

Add worker_ready() synchronization before the rename operation, matching the pattern used in other config_watcher tests. Without this, the filesystem event from the rename could occur before the watcher is fully ready to receive it, causing flaky test failures on macOS where FSEvents timing differs from Linux's inotify.

* test: fix lines_big_chunk_restore race condition

* Wait for daemon-wrote-s2c-chunk event before sending the large command

This avoids a race where the command is sent before the shell is ready to receive input.

* Silence macOS bash deprecation warning via BASH_SILENCE_DEPRECATION_WARNING env var

The warning contains 'd' which caused read_until(b'd') to stop early before reaching "food" in the restore buffer.

* test: skip known-failing tests on macOS

Skip 6 tests that fail on macOS due to platform differences:

- prompt_prefix_zsh, prompt_prefix_fish: hard-coded Linux shell paths
- motd_pager, motd_debounced_pager_*, motd_env_test_pager_*: PTY pager output doesn't reach client on macOS

Update HACKING.md to document skipped tests.

* docs: add safety comments for unsafe FFI calls

* style: apply cargo fmt nightly formatting

---------

Co-authored-by: Selman Kayrancioglu <selmankayrancioglu@gmail.com>
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.

3 participants