From 0a9105063578c73f668b4a6e397941f1f6ceccc7 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Fri, 25 Sep 2026 17:15:02 +0200 Subject: [PATCH] imsd, media: aim call audio at logind's seat0 session, not an account named `user` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit imsd-media defaulted AUDIO_USER to `user`, postmarketOS's default account name, and with no such account a call had no audio on the phone and noise at the far end, the message only in imsd-media's stderr, at each call. A field user found it by trial (alyx report 14, 2026-09-20; ims ledger F14). Have imsd, as packaged (root, on the system bus), resolve the session from logind when a call's media starts — the user session active on seat0 (a greeter is not one) when it has a PipeWire socket — and hand it to imsd-media as AUDIO_USER; set in imsd's own environment to an account's name, AUDIO_USER pins that account instead. imsd as packaged (root, on the system bus) logs the resolution at start, before the first call, and when a call's media starts. No session is an empty AUDIO_USER. imsd-media started without the variable at all keeps `user`: that is an imsd older than 0.3.10, still running while an upgrade replaces the helper under it, whose calls would otherwise be silent until the restart, or a hand run. Tested on the dev phone (KPN) in builds of this series; not every path has run there. --- README.md | 6 ++-- implementations/main.cpp | 70 +++++++++++++++++++++++++++++++++++++++ implementations/media.cpp | 55 +++++++++++++++++++----------- 3 files changed, 108 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 86220c8..b32708f 100644 --- a/README.md +++ b/README.md @@ -146,8 +146,8 @@ busctl --user call net.catcrafts.IMS1 /net/catcrafts/IMS1 net.catcrafts.IMS1 Get On a phone the daemon runs as root on the system bus (policy in `packaging/net.catcrafts.IMS1.conf` — it grants the postmarketOS default -account `user` access; adjust it alongside `AUDIO_USER` if your session -account is named differently), started by `packaging/imsd.service` +account `user` access; adjust it if your session account is named +differently), started by `packaging/imsd.service` after `packaging/ims-pdn-up.sh` has brought up the ims PDN through ModemManager. The bring-up never lets the ims PDN become the phone's IPv6 default route: the PDN's router advertisement is not accepted as a default @@ -354,7 +354,7 @@ its own `retry-after`). | Variable | Default | Meaning | |---|---|---| | `MIC`, `PLAY` | `1`, `1` | uplink mic capture / downlink playout via PipeWire | -| `AUDIO_USER` | `user` | desktop user whose PipeWire session carries call audio (postmarketOS default account) | +| `AUDIO_USER` | *(auto)* | account whose PipeWire session carries call audio. imsd sets it for each call from logind: the user session active on seat0 (`loginctl show-seat seat0`, a greeter does not count) when it has a `/run/user//pipewire-0` socket. Set in imsd's own environment it pins an account instead. imsd as packaged (root, on the system bus) logs the resolution at start and when a call's media starts (`audio session: …`), imsd-media once per call. imsd-media started without the variable at all (by an imsd older than 0.3.10 while an upgrade replaces it, or by hand) tries the account `user`, its old default. | | `GAIN`, `PLAY_GAIN` | `10`, `1.0` | uplink / downlink gain | | `AMR_MODE` | `2` | AMR-WB encoder mode (0–8) | | `DTX` | `0` | encoder discontinuous transmission | diff --git a/implementations/main.cpp b/implementations/main.cpp index 155ab50..c644053 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -31,6 +31,7 @@ reach the bus via g_idle_add, where the calls table + status snapshot live #include #include #include +#include #include #include #include @@ -203,6 +204,64 @@ bool OnLink(const std::string& dev, const std::string& addr) { return found; } +// The account whose PipeWire session carries call audio: AUDIO_USER when +// it names an account, else logind's seat0 session (`loginctl show-seat +// seat0` -> `show-session`) when that is a user session — not phrog's +// greeter — with a PipeWire socket. imsd-media gets the answer as +// AUDIO_USER. As packaged (root, on the system bus) imsd resolves it at +// start, so a wrong name or a missing session is in the journal before the +// first call rather than after two silent rounds of trial (alyx report 14, +// 2026-09-20), and again when a call's media starts, because the answer +// changes when someone logs in. +struct AudioSession { std::string name; unsigned uid = 0; std::string how; }; + +std::string LoginctlProp(std::string_view text, std::string_view key) { + for (auto line : std::views::split(text, '\n')) { + std::string_view l(line); + if (l.size() > key.size() && l.starts_with(key) && l[key.size()] == '=') return std::string(l.substr(key.size() + 1)); + } + return {}; +} + +AudioSession ResolveAudioSession() { + AudioSession s; + passwd pw{}; + passwd* res = nullptr; + char buf[4096]; + std::string want = EnvOr("AUDIO_USER", ""); + if (!want.empty()) { + if (getpwnam_r(want.c_str(), &pw, buf, sizeof buf, &res) == 0 && res) { + s.name = want; s.uid = res->pw_uid; s.how = "AUDIO_USER"; + return s; + } + Log(std::format("AUDIO_USER '{}' is not an account; using logind's seat0 session instead", want)); + } + auto seat = RunCapture({"loginctl", "show-seat", "seat0", "-p", "ActiveSession", "--value"}); + std::string id = seat.out; + while (!id.empty() && (id.back() == '\n' || id.back() == ' ')) id.pop_back(); + if (seat.rc != 0 || id.empty()) { s.how = "no session on seat0"; return s; } + auto sess = RunCapture({"loginctl", "show-session", id, "-p", "Name", "-p", "Class", "-p", "User"}); + std::string name = LoginctlProp(sess.out, "Name"), cls = LoginctlProp(sess.out, "Class"), uid = LoginctlProp(sess.out, "User"); + unsigned u = 0; + auto [end, err] = std::from_chars(uid.data(), uid.data() + uid.size(), u); + if (sess.rc != 0 || name.empty() || err != std::errc{} || end != uid.data() + uid.size()) { s.how = std::format("seat0 session {} not readable", id); return s; } + if (!cls.starts_with("user")) { s.how = std::format("seat0 session {} is {}'s {} session, nobody logged in", id, name, cls); return s; } + std::error_code ec; + if (!std::filesystem::exists(std::format("/run/user/{}/pipewire-0", u), ec)) { s.how = std::format("seat0 session {} ({}) has no PipeWire socket in /run/user/{}", id, name, u); return s; } + s.name = name; s.uid = u; s.how = std::format("logind seat0 session {}", id); + return s; +} + +std::string AudioSessionLine(const AudioSession& s) { + if (!s.name.empty()) return std::format("audio session: {} (uid {}, {})", s.name, s.uid, s.how); + return std::format("audio session: none — {}", s.how); +} + +void LogAudioSession() { + AudioSession s = ResolveAudioSession(); + Log(AudioSessionLine(s) + (s.name.empty() ? "; looked up again at each call, set AUDIO_USER to pin one" : "")); +} + // ---- sockaddr helper ------------------------------------------------------ socklen_t MakeAddr(std::string_view ip, int port, sockaddr_storage& ss) { std::memset(&ss, 0, sizeof ss); @@ -2195,8 +2254,18 @@ private: mediaBin_, local_, std::to_string(rtpPort_), leg.remoteIp, std::to_string(leg.remotePort), std::to_string(leg.payloadType), "86400", outBase}; + AudioSession session; + if (geteuid() == 0) { + session = ResolveAudioSession(); + Log(AudioSessionLine(session) + (session.name.empty() ? "; this call has no audio" : "")); + } pid_t pid = fork(); if (pid == 0) { + if (geteuid() == 0) { + // no session is said with an empty value: imsd-media reads a + // missing variable as a caller older than 0.3.10 + setenv("AUDIO_USER", session.name.c_str(), 1); + } setenv("MIC", EnvOr("MIC", "1").c_str(), 1); setenv("PLAY", EnvOr("PLAY", "1").c_str(), 1); setenv("GAIN", EnvOr("GAIN", "10").c_str(), 1); @@ -2384,6 +2453,7 @@ int main(int argc, char** argv) { return 1; } Log(std::format("LOCAL={} P-CSCF=[{}]:{}", local, EnvOr("PCSCF", "(unset)"), EnvOr("PCSCF_PORT", "5060"))); + if (!session) LogAudioSession(); Engine engine; engine.SetLocal(local); diff --git a/implementations/media.cpp b/implementations/media.cpp index b7d5964..7f37fa7 100644 --- a/implementations/media.cpp +++ b/implementations/media.cpp @@ -410,41 +410,56 @@ std::size_t RtpPayloadOffset(std::span pkt) { return off; } -// The desktop user's uid, resolved ONCE and thread-safely. getpwnam() hands -// out one static buffer, and the mic and playout threads spawn their pw -// helpers at the same instant — the race sent both into /run/user/0, where -// no PipeWire session lives, and a call was answered into static (dev phone, -// 2026-09-08 20:01: tx_mic=0 rx_played=0 qdrop=883). Logged per call so a -// journal shows which session the helpers were aimed at. -uid_t AudioUid() { +// The desktop session whose PipeWire instance carries call audio, resolved +// ONCE and thread-safely: getpwnam() hands out one static buffer, and the +// mic and playout threads spawn their pw helpers at the same instant — the +// race sent both into /run/user/0, where no PipeWire session lives, and a +// call was answered into static (dev phone, 2026-09-08 20:01: tx_mic=0 +// rx_played=0 qdrop=883). AUDIO_USER names the account: imsd sets it per +// call from logind's seat0 session (or its own AUDIO_USER), and sets it +// empty when nobody is logged in. The packaged default used to be +// postmarketOS's account name `user`: a unit whose account was called +// otherwise got no audio and no message (alyx report 14, 2026-09-20). +// That default stays for a caller that does not set the variable at all — +// an imsd older than 0.3.10, still running while an upgrade replaces this +// binary, or a hand run — so its calls keep their audio. +// Logged per call so a journal shows which session the helpers were aimed at. +struct AudioSession { uid_t uid = 0; std::string name; }; + +const AudioSession& Session() { static std::once_flag once; - static uid_t uid = 0; + static AudioSession s; std::call_once(once, [] { - std::string user = EnvOr("AUDIO_USER", "user"); passwd pw{}; passwd* res = nullptr; char buf[4096]; - if (getpwnam_r(user.c_str(), &pw, buf, sizeof buf, &res) == 0 && res) uid = res->pw_uid; + const char* given = std::getenv("AUDIO_USER"); + std::string want = given ? given : "user"; + if (!given) std::println(std::cerr, "imsd-media: no AUDIO_USER in the environment (a caller older than 0.3.10, or a hand run); trying the account `user`"); + if (want.empty()) + std::println(std::cerr, "imsd-media: AUDIO_USER empty (no session to aim at); the helpers run as uid 0 (no audio)"); + else if (getpwnam_r(want.c_str(), &pw, buf, sizeof buf, &res) == 0 && res) { s.uid = res->pw_uid; s.name = want; } else - std::println(std::cerr, "imsd-media: AUDIO_USER '{}' not found; no PipeWire session for uid 0", user); - std::println(std::cerr, "imsd-media: audio session: {} -> /run/user/{}", user, uid); + std::println(std::cerr, "imsd-media: AUDIO_USER '{}' is not an account; the helpers run as uid 0 (no audio)", want); + std::println(std::cerr, "imsd-media: audio session: {} -> /run/user/{}", s.name.empty() ? std::string("(none)") : s.name, s.uid); }); - return uid; + return s; } -// Spawn pw-record/pw-play in the desktop user's PipeWire session (sudo -u -// when run as root). AUDIO_USER names the session owner; the default "user" -// is postmarketOS's standard account. `toChild` true = we write the child's -// stdin (pw-play); false = we read its stdout (pw-record). Returns {pid, fd}. +// Spawn pw-record/pw-play in the desktop session's PipeWire instance (sudo -u +// when run as root; as ourselves when there is no session to aim at). +// `toChild` true = we write the child's stdin (pw-play); false = we read its +// stdout (pw-record). Returns {pid, fd}. struct Child { pid_t pid = -1; int fd = -1; }; Child SpawnPw(bool play, bool toChild, int rate) { int pipefd[2]; if (pipe(pipefd) != 0) return {}; std::vector argv; if (geteuid() == 0) { - std::string user = EnvOr("AUDIO_USER", "user"); - argv = {"sudo", "-u", user, "env", - std::format("XDG_RUNTIME_DIR=/run/user/{}", AudioUid())}; + const AudioSession& session = Session(); + if (!session.name.empty()) + argv = {"sudo", "-u", session.name, "env", + std::format("XDG_RUNTIME_DIR=/run/user/{}", session.uid)}; } const char* tool = play ? "pw-play" : "pw-record"; const char* lat = play ? "40ms" : "20ms";