Description
GlobalActor::connect_api passes the embedded Python interpreter the API's TCP port, so the interpreter connects to its own process through the CAF middleman:
// src/global/src/global_actor.cpp
mail(connect_atom_v, port_).request(embedded_python, infinite)
# src/embedded_python/src/embedded_python.cpp -> EmbeddedPython::connect(int)
XSTUDIO = Connection(debug=False)
XSTUDIO.connect_remote("127.0.0.1", <port>)
That is a real loopback TCP connection from the process to itself, and the OS is free to tear it down. macOS does so on system sleep. Nothing reconnects it, so after a wake every Python plugin is permanently cut off from the application hosting it, while the rest of the UI keeps working because it uses in-process actors.
Symptoms
- Python panels stop responding. Clicking a control runs the QML handler, but
attribute_changed never reaches Python.
ModuleAttribute.set_value() and connection.send() appear to succeed — py_send hands the message to a dead proxy and returns.
- Every
request_receive blocks for the full default_timeout_ms and then raises TimeoutError("Dequeue timeout"). Since py_request uses caf::infinite, CAF never produces an error of its own, so it reads as a hang rather than a failure.
- The native UI, playback and viewport are unaffected, which makes this look like a plugin bug rather than a transport one.
- Nothing is logged.
Connection.connected is set once in negotiate() and is never cleared by a link failure, and no caf.middleman.* heartbeat is configured, so nothing observes the disconnection.
Reproduction (macOS)
-
Launch xSTUDIO with Python plugins enabled.
-
Inspect the sockets — note the loopback pair, both ends owned by the same PID:
$ lsof -a -nP -p $(pgrep -f xstudio.bin) -iTCP
xstudio.b 27724 fd 8u TCP 127.0.0.1:14441 (LISTEN)
xstudio.b 27724 fd 20u TCP 127.0.0.1:59506->127.0.0.1:14441 (ESTABLISHED)
xstudio.b 27724 fd 21u TCP 127.0.0.1:14441->127.0.0.1:59506 (ESTABLISHED)
-
Sleep the machine for ~5 minutes, then wake it.
-
Run the same command against the same, unrestarted process:
$ lsof -a -nP -p 27724 -iTCP
xstudio.b 27724 fd 8u TCP 127.0.0.1:14441 (LISTEN)
Both file descriptors of the pair are closed outright — not half-open, not TIME_WAIT. The listener survives.
-
Any Python plugin interaction now fails as described above, for the remaining lifetime of the session. Only a restart recovers it.
Suggested fix
EmbeddedPythonActor already has a connect_atom/caf::actor handler that calls Connection.connect_local(), and py_context::connect_local() already exists — but nothing ever sends it an actor, so that path is currently dead code. Passing apia_ instead of port_ selects it and removes the socket entirely. APIActor answers the whole negotiate() handshake (version, application mode, authenticate), and its authenticate_atom handler admits any sender on the same node, so a local connect succeeds.
The API port is still published, so external clients (xstudiopy, xstudio_control) are unaffected — only the embedded interpreter stops using it.
I have this running locally and will open a PR shortly.
Environment
- xSTUDIO 1.3.0
- macOS on Apple silicon (Darwin 25.5.0)
- Not inherently macOS-specific: any OS that can drop a loopback TCP connection will produce the same result. macOS system sleep is simply a reliable trigger.
Description
GlobalActor::connect_apipasses the embedded Python interpreter the API's TCP port, so the interpreter connects to its own process through the CAF middleman:That is a real loopback TCP connection from the process to itself, and the OS is free to tear it down. macOS does so on system sleep. Nothing reconnects it, so after a wake every Python plugin is permanently cut off from the application hosting it, while the rest of the UI keeps working because it uses in-process actors.
Symptoms
attribute_changednever reaches Python.ModuleAttribute.set_value()andconnection.send()appear to succeed —py_sendhands the message to a dead proxy and returns.request_receiveblocks for the fulldefault_timeout_msand then raisesTimeoutError("Dequeue timeout"). Sincepy_requestusescaf::infinite, CAF never produces an error of its own, so it reads as a hang rather than a failure.Connection.connectedis set once innegotiate()and is never cleared by a link failure, and nocaf.middleman.*heartbeat is configured, so nothing observes the disconnection.Reproduction (macOS)
Launch xSTUDIO with Python plugins enabled.
Inspect the sockets — note the loopback pair, both ends owned by the same PID:
Sleep the machine for ~5 minutes, then wake it.
Run the same command against the same, unrestarted process:
Both file descriptors of the pair are closed outright — not half-open, not
TIME_WAIT. The listener survives.Any Python plugin interaction now fails as described above, for the remaining lifetime of the session. Only a restart recovers it.
Suggested fix
EmbeddedPythonActoralready has aconnect_atom/caf::actorhandler that callsConnection.connect_local(), andpy_context::connect_local()already exists — but nothing ever sends it an actor, so that path is currently dead code. Passingapia_instead ofport_selects it and removes the socket entirely.APIActoranswers the wholenegotiate()handshake (version, application mode, authenticate), and itsauthenticate_atomhandler admits any sender on the same node, so a local connect succeeds.The API port is still published, so external clients (
xstudiopy,xstudio_control) are unaffected — only the embedded interpreter stops using it.I have this running locally and will open a PR shortly.
Environment