| 312 | } |
| 313 | |
| 314 | void DaxIqWorker::createPipe(int channel, int sampleRate) |
| 315 | { |
| 316 | #ifndef Q_OS_WIN |
| 317 | int idx = channel - 1; |
| 318 | if (idx < 0 || idx >= DaxIqModel::NUM_CHANNELS) return; |
| 319 | if (m_pipeFds[idx] >= 0) destroyPipe(channel); |
| 320 | |
| 321 | const QString pipePath = iqPipePath(channel); |
| 322 | if (pipePath.isEmpty()) { |
| 323 | qCWarning(lcAudio) << "DaxIqWorker: cannot resolve IQ FIFO dir for ch" << channel; |
| 324 | return; |
| 325 | } |
| 326 | |
| 327 | // Create the owner-only FIFO ourselves first, THEN load the pipe-source. |
| 328 | // (1) mkfifo-first removes the create-race: the old code fire-and-forgot |
| 329 | // `pactl` and raced a 200ms sleep to open a FIFO module-pipe-source had |
| 330 | // not created yet -> ENOENT "cannot open IQ pipe", no node ever appeared. |
| 331 | // (2) 0600 under $XDG_RUNTIME_DIR/aethersdr (not /tmp/aethersdr-iq-N.pipe, |
| 332 | // which was prw-rw-rw-) closes the world-writable-FIFO IQ-injection |
| 333 | // vector, mirroring the DAX-audio hardening (GHSA-x8xf-4g5v-ppf9). |
| 334 | if (!makeOwnedFifo(pipePath)) { |
| 335 | qCWarning(lcAudio) << "DaxIqWorker: mkfifo failed for" << pipePath; |
| 336 | return; |
| 337 | } |
| 338 | |
| 339 | // Load module-pipe-source SYNCHRONOUSLY so we know it succeeded and capture |
| 340 | // the module index for a clean unload. source_properties is single-quoted so |
| 341 | // the spaced device.description survives pipewire-pulse's module-arg parser |
| 342 | // (mirrors the DAX RX/TX naming fix); node.description drives the label shown |
| 343 | // in qpwgraph / JACK / WSJT-X. |
| 344 | const quint32 modIdx = runPactlSync({ |
| 345 | "load-module", "module-pipe-source", |
| 346 | QStringLiteral("source_name=aethersdr-iq-%1").arg(channel), |
| 347 | QStringLiteral("file=%1").arg(pipePath), |
| 348 | QStringLiteral("format=float32le"), |
| 349 | QStringLiteral("rate=%1").arg(sampleRate), |
| 350 | QStringLiteral("channels=2"), |
| 351 | QStringLiteral("source_properties='device.description=\"AetherSDR DAX IQ %1\"'").arg(channel), |
| 352 | }); |
| 353 | if (modIdx == 0) { |
| 354 | ::unlink(pipePath.toLocal8Bit().constData()); |
| 355 | qCWarning(lcAudio) << "DaxIqWorker: pactl load-module failed for IQ ch" << channel; |
| 356 | return; |
| 357 | } |
| 358 | m_pipeModuleIdx[idx] = modIdx; |
| 359 | |
| 360 | // Open the write end. O_RDWR (not O_WRONLY) never ENXIOs even if the module's |
| 361 | // read side has not attached yet; we only ever write this fd. |
| 362 | int fd = ::open(pipePath.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK); |
| 363 | if (fd < 0) { |
| 364 | qCWarning(lcAudio) << "DaxIqWorker: cannot open IQ pipe" << pipePath |
| 365 | << strerror(errno); |
| 366 | runPactlSync({"unload-module", QString::number(modIdx)}); |
| 367 | ::unlink(pipePath.toLocal8Bit().constData()); |
| 368 | m_pipeModuleIdx[idx] = 0; |
| 369 | return; |
| 370 | } |
| 371 | m_pipeFds[idx] = fd; |
no test coverage detected