Skip to content

macOS support (for the fancy peeps with nice furniture)#406

Merged
goodboy merged 49 commits intomainfrom
macos_support
Mar 10, 2026
Merged

macOS support (for the fancy peeps with nice furniture)#406
goodboy merged 49 commits intomainfrom
macos_support

Conversation

@goodboy
Copy link
Copy Markdown
Owner

@goodboy goodboy commented Oct 6, 2025

A more series effort toward #404 and getting macOS running as part of
our CI!

There's been some new collabs entering the scene who are helping with
this so I will be piecewise making updates here as we work through
various fixes on other git-service-platforms.


Set of patches which make #342 green

  • (064deec) Use platformdirs for .config.get_rt_dir()
  • (fdf63e3) original non-fix for UDS tpt.
  • (b4bc127) mkt-rent-subdirs fix for ._state.get_rt_dir() use.
  • (a0730ae) shm buffer file name len limit fix from piker
    • originally by @Dnks
    • (2b5a33b) similar fix for ShmList internal keying.
  • (e838b83) proper fix for UDS on macos by @goodboy.
    • (4690b24) documenting the magic-constants use in ^ per the
      darwin kernel sources.
  • (6aabe75) a prompt-flush fix for bash on macos similar to what
    we have already for xonsh; makes the debugger test suites pass.

Todo pre-Landing of #342 history on top of this,

  • cleanup history in Macos in ci #342.

    • rebase Macos in ci #342 onto this history.
    • squash various (marked) commits
    • breakup, factor-out/in any pkg/test-harness mgmt adjustments
      which are unclearly mixed together.
  • via PR, merge Macos in ci #342 history into this.

  • ensure the GH ci.yml is in best shape it can be.

    • (88b0848) merges the jobs into a single matrix for less overall
      CI entries B)
  • all green on CI runs for this branch 🙏

@goodboy goodboy added non-linux for those who prefer walled gardens (macos, windows, etc.) macos_related gotta tailor to them job bros i suppose labels Oct 6, 2025
@goodboy goodboy changed the title Use platformdirs for .config.get_rt_dir() macOS support (for the fancy peeps with nice furniture) Oct 6, 2025
Copilot AI review requested due to automatic review settings March 5, 2026 02:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Tractor’s runtime-directory resolution to be cross-platform (including macOS) by switching from direct XDG_RUNTIME_DIR usage to platformdirs.

Changes:

  • Add platformdirs as a runtime dependency (and lock it in uv.lock).
  • Rework tractor._state.get_rt_dir() to use platformdirs.user_runtime_dir() instead of XDG_RUNTIME_DIR.
  • Update the get_rt_dir() docstring to reflect the new behavior.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

File Description
tractor/_state.py Switch runtime-dir selection to platformdirs and adjust get_rt_dir() API/behavior.
pyproject.toml Add platformdirs>=4.4.0 to project dependencies.
uv.lock Lock platformdirs and add it to the resolved dependency set.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tractor/_state.py Outdated
Comment on lines +197 to +198
if not rt_dir.is_dir():
rt_dir.mkdir()
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

rt_dir.mkdir() can fail on macOS if platformdirs.user_runtime_dir() returns a nested path whose parents don’t exist, and it can also race between processes (TOCTOU) causing FileExistsError. Use mkdir(parents=True, exist_ok=True) (or platformdirs.user_runtime_dir(..., ensure_exists=True) if available) so directory creation is robust.

Suggested change
if not rt_dir.is_dir():
rt_dir.mkdir()
rt_dir.mkdir(parents=True, exist_ok=True)

Copilot uses AI. Check for mistakes.
Comment thread tractor/_state.py Outdated
Comment on lines +184 to +186
On linux we take use a `'${XDG_RUNTIME_DIR}/tractor/` subdir by
default but equivalents are mapped for each platform using
the lovely `platformdirs`.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The docstring has a couple of issues: “take use” reads like a typo, and the quoting/backtick around ${XDG_RUNTIME_DIR}/tractor/ is unbalanced (opening ' without a closing one). Please fix the wording/markup so the documentation renders correctly and is unambiguous.

Suggested change
On linux we take use a `'${XDG_RUNTIME_DIR}/tractor/` subdir by
default but equivalents are mapped for each platform using
the lovely `platformdirs`.
On linux we use the `${XDG_RUNTIME_DIR}/tractor/` subdir by
default, but equivalents are mapped for each platform using
the `platformdirs` library.

Copilot uses AI. Check for mistakes.
Comment thread tractor/_state.py Outdated
),
)
if subdir:
rt_dir: Path = rt_dir / subdir
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Allowing subdir to be a Path means callers can pass an absolute path (or .. segments) and cause get_rt_dir() to return a location outside the intended runtime directory (rt_dir / subdir discards the base if subdir is absolute). If subdir can ever be influenced by external input, validate it is a relative path (and consider rejecting ..), or keep the parameter as a simple relative str.

Suggested change
rt_dir: Path = rt_dir / subdir
# Normalize and validate that `subdir` is a relative path
# without any parent-directory ("..") components, to prevent
# escaping the runtime directory.
subdir_path = subdir if isinstance(subdir, Path) else Path(subdir)
if subdir_path.is_absolute():
raise ValueError('subdir must be a relative path')
if any(part == '..' for part in subdir_path.parts):
raise ValueError("subdir must not contain '..' components")
rt_dir = rt_dir / subdir_path

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yup good idea, adding this.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tractor/ipc/_shm.py
Comment on lines +214 to +222
if len(key) <= max_len:
return key

_hash: str = hashlib.sha256(
key.encode()
).hexdigest()

hash_len: int = (
(max_len - 1)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

_shorten_key_for_macos() ignores the provided prefix/suffix when len(key) <= max_len (it returns key unchanged). This causes collisions on macOS in _make_token() because shm_name, shm_first_index_name, and shm_last_index_name can become identical for “short” keys, and it also drops the prefix for open_shm_list(). Adjust the early-return logic to include prefix + key + suffix and ensure the length check accounts for the leading '/' added by SharedMemory on POSIX as well as the prefix/suffix lengths.

Suggested change
if len(key) <= max_len:
return key
_hash: str = hashlib.sha256(
key.encode()
).hexdigest()
hash_len: int = (
(max_len - 1)
# Account for the leading '/' added by POSIX SharedMemory names
usable_len: int = max_len - 1
full_key: str = f"{prefix}{key}{suffix}"
if len(full_key) <= usable_len:
return full_key
_hash: str = hashlib.sha256(
key.encode()
).hexdigest()
hash_len: int = (
usable_len

Copilot uses AI. Check for mistakes.
Comment thread tractor/ipc/_shm.py
Comment on lines +912 to +917
if platform.system() == 'Darwin':
key: str = _shorten_key_for_macos(
key=key,
prefix=prefix,
)

Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

On macOS open_shm_list() may shorten the provided key, so shml.key (and the value passed to other processes) can differ from the original input. Existing tests/callers that assert equality with the original key will fail on Darwin for long keys (e.g. UUID-based keys). Consider documenting this behavior and/or preserving the original key separately so API consumers don’t assume ShmList.key == input key across platforms.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is coming as part of merging #342 here 😉

Comment thread tractor/_state.py

if not rt_dir.is_dir():
rt_dir.mkdir(
parents=True,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

get_rt_dir() uses a check-then-create pattern (if not rt_dir.is_dir(): rt_dir.mkdir(...)) without exist_ok=True. In concurrent startup (multiple tasks/processes) this can raise FileExistsError if another process creates the directory between the check and the mkdir. Use mkdir(parents=True, exist_ok=True) (or catch FileExistsError) to make this race-safe.

Suggested change
parents=True,
parents=True,
exist_ok=True,

Copilot uses AI. Check for mistakes.
Comment thread tractor/ipc/_uds.py Outdated
# NOTE known to at least works on,
# - macos
else:
peer_pid: int = get_peer_pid(sock)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

get_peer_pid() returns int | None, but in the non-Linux branch peer_pid is annotated as int. If get_peer_pid() fails, peer_pid will be None and the annotation becomes inaccurate (and will trip static type-checking). Consider annotating peer_pid as int|None here (or handling the None case explicitly) since UDSAddress.maybe_pid already supports None.

Suggested change
peer_pid: int = get_peer_pid(sock)
peer_pid: int|None = get_peer_pid(sock)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yup and added a warning.

Comment thread pyproject.toml
Comment on lines 9 to 13
version = "0.1.0a6dev0"
description = 'structured concurrent `trio`-"actors"'
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
requires-python = ">= 3.11"
requires-python = ">=3.12, <3.14"
readme = "docs/README.rst"
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

requires-python is now >=3.12, <3.14, but the project classifiers still include Programming Language :: Python :: 3.11. This metadata mismatch can confuse package index consumers and tooling; please update the classifiers to reflect the supported Python versions (or adjust requires-python if 3.11 is still intended).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch.

goodboy added a commit that referenced this pull request Mar 8, 2026
Per the `copilot` review,
#406 (review)

now we also,
- pass `exists_ok=True` to `.mkdir()` to avoid conc races.
- expose `appname: str` param for caller override.
- normalize `subdir` to avoid escaping the base rt-dir location.
goodboy added a commit that referenced this pull request Mar 8, 2026
Per the `copilot` review,
#406 (review)

now we also,
- pass `exists_ok=True` to `.mkdir()` to avoid conc races.
- expose `appname: str` param for caller override.
- normalize `subdir` to avoid escaping the base rt-dir location.
goodboy and others added 11 commits March 8, 2026 19:16
Thanks to the `tox`-dev community for such a lovely pkg which seems to
solves all the current cross-platform user-dir problems B)

Also this,
- now passes `platformdirs.user_runtime_dir(appname='tractor')`
  and allows caller to pass an optional `subdir` under `tractor/`
  if desired.
- drops the `.config._rtdir: Path` mod var.
- bumps the lock file with the new dep.
Make socket credential imports platform-conditional in `.ipc._uds`.
- Linux: use `SO_PASSCRED`/`SO_PEERCRED` from socket module
- macOS: use `LOCAL_PEERCRED` (0x0001) instead, no need for `SO_PASSCRED`
- Conditionally call `setsockopt(SO_PASSCRED)` only on Linux

Fixes AttributeError on macOS where SO_PASSCRED doesn't exist.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Move the multi-platorm-supporting conditional/dynamic `socket` constant
imports to *after* the main cross-platform ones.
Also add constant typing and reformat comments a bit for the macOS case.
Adapt the `PSHMNAMLEN` fix from `piker.data._sharedmem` (orig commit
96fb79ec thx @Dnks!) to `tractor.ipc._shm` accounting for the
module-local differences:

- Add `hashlib` import for sha256 key hashing
- Add `key: str|None` field to `NDToken` for storing
  the original descriptive key separate from the
  (possibly shortened) OS-level `shm_name`
- Add `__eq__()`/`__hash__()` to `NDToken` excluding
  the `key` field from identity comparison
- Add `_shorten_key_for_macos()` using `t_` prefix
  (vs piker's `p_`) with 16 hex chars of sha256
- Use `platform.system() == 'Darwin'` in `_make_token()`
  (tractor already imports the `platform` module vs
  piker's `sys.platform`)
- Wrap `shm_unlink()` in `ShmArray.destroy()` with
  `try/except FileNotFoundError` for teardown races
  (was already done in `SharedInt.destroy()`)
- Move token creation before `SharedMemory()` alloc in
  `open_shm_ndarray()` so `token.shm_name` is used
  as the OS-level name
- Use `lookup_key` pattern in `attach_shm_ndarray()`
  to decouple `_known_tokens` dict key from OS name

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Same problem as for the `ShmArray` tokens, so tweak and reuse
the `_shorten_key_for_macos()` helper and call it from
`open_shm_list()` similarly.

Some tweaks/updates to the various helpers,
- support `prefix/suffix` inputs and if provided take their lengths and
  subtract them from the known *macOS shm_open() has a 31 char limit
  (PSHMNAMLEN)* when generating and using the `hashlib.sha256()` value
  which overrides (for now..) wtv `key` is passed by the caller.
- pass the appropriate `suffix='_first/_last'` values for the `ShmArray`
  token generators case.
- add a `prefix: str = 'shml_'` param to `open_shm_list()`.
- better log formatting with `!r` to report any key shortening.
Though it was a good (vibed) try by @Dnks, the previous "fix" was not
actually adding unix socket support but merely sidestepping a crash due
to `get_peer_info()`'s impl never going to work on MacOS (and it was
never intended to).

This patch instead solves the underlying issue by implementing a new
`get_peer_pid()` helper which does in fact retrieve the peer's PID in
a more generic/cross-platform way (:fingers_crossed:); much thanks to
the linked SO answer for this solution!

Impl deats,
- add `get_peer_pid()` and call it from
  `MsgpackUDSStream.get_stream_addrs()` when we detect a non-'linux'
  platform, OW use the original soln: `get_stream_addrs()`.
- add a new case for the `match (peername, sockname)` with a
  `case (str(), str()):` which seems to at least work on macos.
- drop all the `LOCAL_PEERCRED` dynamic import branching since it was
  never needed and was never going to work.
Per the questionable `copilot` review which is detailed for follow up in
#418. These constants are
directly linked from the kernel sources fwiw.
Per the `copilot` review,
#406 (review)

now we also,
- pass `exists_ok=True` to `.mkdir()` to avoid conc races.
- expose `appname: str` param for caller override.
- normalize `subdir` to avoid escaping the base rt-dir location.
goodboy added 23 commits March 9, 2026 19:30
There's a very sloppy registrar-actor-bootup syncing approach used in
this fixture (basically just guessing how long to sleep to wait for it
to init and bind the registry socket) using a `global _PROC_SPAWN_WAIT`
that needs to be made more reliable. But, for now i'm just playing along
with what's there to try and make less CI runs flaky by,

- sleeping *another* 1s when run from non-linux CI.
- reporting stdout (if any) alongside stderr on teardown.
- not strictly requiring a `proc.returncode == -2` indicating successful
  graceful cancellation via SIGINT; instead we now error-log and only
  raise the RTE on `< 0` exit code.
  * though i can't think of why this would happen other then an
    underlying crash which should propagate.. but i don't think any test
    suite does this intentionally rn?
  * though i don't think it should ever happen, having a CI run
    "error"-fail bc of this isn't all that illuminating, if there is
    some weird `.returncode == 0` termination case it's likely not
    a failure?

For later, see the new todo list; we should sync to some kind of "ping"
polling of the tpt address if possible which is already easy enough for
TCP reusing an internal closure from `._root.open_root_actor()`.
Via ensuring `all(mark.args)` on wtv expressions are arg-passed to the
mark decorator; use it to skip the `test_subactor_breakpoint` suite when
`ctlc=True` since it seems too unreliable in CI.
- convert all doc-strings to `'''` multiline style.
- rename `nursery` -> `an`, `n` -> `tn` to match
  project-wide conventions.
- add type annotations to fn params (fixtures, test
  helpers).
- break long lines into multiline style for fn calls,
  assertions, and `parametrize` decorator lists.
- add `ids=` to `@pytest.mark.parametrize`.
- use `'` over `"` for string literals.
- add `from typing import Callable` import.
- drop spurious blank lines inside generators.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
It seems something is up with their VM-img or wtv bc i keep increasing
the subproc timeout and nothing is changing. Since i can't try
a `-xlarge` one without paying i'm just muting this test for now.
@goodboy goodboy mentioned this pull request Mar 10, 2026
4 tasks
goodboy and others added 3 commits March 9, 2026 20:33
- add `"Operating System :: MacOS"` classifier.
- add macOS bullet to README's TODO/status section.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Drop the separate `testing-macos` job and add
`macos-latest` to the existing OS matrix; bump
timeout to 16 min to accommodate macOS runs.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
@goodboy goodboy merged commit c1c4d85 into main Mar 10, 2026
3 checks passed
@goodboy goodboy deleted the macos_support branch March 10, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

IPC and transport macos_related gotta tailor to them job bros i suppose non-linux for those who prefer walled gardens (macos, windows, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants