Skip to content

Send signal as a string in ClientProcess.kill (fixes "missing signal in xpc message")#1997

Open
chengboonrong wants to merge 1 commit into
apple:mainfrom
chengboonrong:claude/open-source-contribution-055112
Open

Send signal as a string in ClientProcess.kill (fixes "missing signal in xpc message")#1997
chengboonrong wants to merge 1 commit into
apple:mainfrom
chengboonrong:claude/open-source-contribution-055112

Conversation

@chengboonrong

Copy link
Copy Markdown

Summary

ClientProcess.kill(_:) never delivered a signal: every call failed with
invalidArgument: "missing signal in xpc message".

Signals cross the client→server boundary as an XPC dictionary value. There are
two client senders and one server reader:

  • ContainerClient.kill (the container kill CLI) sends the signal as a
    string name → the server reads a string → works.
  • ClientProcess.kill — used by the public ContainerAPIClient and by the
    foreground CLI's signal forwarding — sent an Int64. The server reads
    .signal with xpc_dictionary_get_string, which returns nil for an integer
    value, so the request was always rejected.

Because ClientProcess.kill also backs the foreground CLI's ^C / SIGWINCH
forwarding, this broke interactive -t containers (^C couldn't stop them) and
spammed stderr with the same error on terminal resize.

Fix

Send the canonical signal name, matching the already-working
ContainerClient.kill path:

request.set(key: .signal, value: Signal.platformName(signal) ?? "\(signal)")

The server resolves signal strings against the Linux signal table, so
sending the name (rather than the raw macOS number) also keeps signals whose
numbers differ between macOS and Linux correct — e.g. host SIGUSR1=30 →
"USR1" → Linux 10, instead of being misdelivered as Linux SIGPWR=30. Falls
back to the numeric string for real-time signals that have no platform name.

Tests

Adds SignalWireFormatTests, which replicates the exact value the client puts
on the wire and the exact resolution the server performs, covering common
signals, the platform-divergent case (SIGUSR1), and the non-empty-string
contract.

Verified locally: swift build, swift format lint --strict, and the new
tests all pass.

Fixes #1941
Fixes #1876
Fixes #1747

🤖 Generated with Claude Code

@chengboonrong
chengboonrong force-pushed the claude/open-source-contribution-055112 branch 2 times, most recently from ef05b81 to d216f25 Compare July 23, 2026 13:25
The API server reads the `.signal` XPC key as a string (via
`xpc_dictionary_get_string`) and resolves it against the Linux signal
table, but `ClientProcess.kill(_:)` wrote it with
`set(key: .signal, value: Int64(signal))`. `xpc_dictionary_get_string`
returns nil for an integer value, so every signal sent through this
path was rejected with `invalidArgument: "missing signal in xpc
message"`.

This path is used both by the public `ContainerAPIClient`
(`ClientProcess.kill`) and by the foreground CLI's signal forwarding,
so `^C` on a `-t` container and `SIGWINCH` terminal-resize forwarding
were both broken.

Send the canonical signal name instead, matching the already-working
`ContainerClient.kill` path. Because the server resolves names against
the Linux table, sending the name (rather than the raw macOS number)
also keeps signals whose numbers differ between macOS and Linux
correct (e.g. host SIGUSR1=30 -> "USR1" -> Linux 10, instead of Linux
SIGPWR=30). Falls back to the numeric string for real-time signals
that have no platform name.

Fixes apple#1941
Fixes apple#1876
Fixes apple#1747

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Chris Cheng <chengchris8715@gmail.com>
@chengboonrong
chengboonrong force-pushed the claude/open-source-contribution-055112 branch from d216f25 to 47f23e4 Compare July 23, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment