/* * SPDX-License-Identifier: GPL-3.0-or-later */ #include "plumber.h" #include "ninep.h" #include "plumbmsg.h" #include #include #include #include #include // Canonicalise the X11 display the way plan9 getns() does: "xxx:0.0" -> "xxx:0" // and '/' -> '_'. Only the trailing ".0" is stripped. static QString canonicalDisplay(QString disp) { const int colon = disp.lastIndexOf(QLatin1Char(':')); if (colon >= 0) { int p = colon + 1; while (p < disp.size() && disp[p].isDigit()) p++; if (disp.mid(p) == QLatin1String(".0")) disp.truncate(p); } disp.replace(QLatin1Char('/'), QLatin1Char('_')); return disp; } QString Plumber::socketPath() { const QByteArray ns = qgetenv("NAMESPACE"); QString nsDir; if (!ns.isEmpty()) { nsDir = QString::fromLocal8Bit(ns); } else { const QByteArray disp = qgetenv("DISPLAY"); if (disp.isEmpty()) return QString(); // no namespace resolvable QString user = QString::fromLocal8Bit(qgetenv("USER")); if (user.isEmpty()) user = QString::fromLocal8Bit(qgetenv("LOGNAME")); nsDir = QStringLiteral("/tmp/ns.%1.%2") .arg(user, canonicalDisplay(QString::fromLocal8Bit(disp))); } return nsDir + QStringLiteral("/plumb"); } // Current user name for the 9P attach static QString currentUser() { QString user = QString::fromLocal8Bit(qgetenv("USER")); if (user.isEmpty()) user = QString::fromLocal8Bit(qgetenv("LOGNAME")); if (user.isEmpty()) user = QStringLiteral("none"); return user; } // --------------------------------------------------------------------------- // PlumbReader: owns a NineP session on a named port, loops on blocking reads. // Lives on its own QThread. Emits parsed messages via queued signals so the // Plumber (GUI thread) can act on them. // --------------------------------------------------------------------------- class PlumbReader : public QThread { Q_OBJECT public: explicit PlumbReader(const QString &socketPath, const QString &portName, QObject *parent = nullptr) : QThread(parent) , m_socketPath(socketPath) , m_portName(portName) { } void stop() { m_stop.storeRelaxed(1); } Q_SIGNALS: void message(const QString &data, const QString &addr, const QString &wdir); void failed(const QString &message); protected: void run() override { NineP nine; if (!nine.connectAndAttach(m_socketPath, currentUser())) { Q_EMIT failed(QStringLiteral("plumb connect: %1").arg(nine.errorString())); return; } static constexpr uint32_t PortFid = 1; if (!nine.walk(PortFid, m_portName) || !nine.open(PortFid, NineP::OREAD)) { Q_EMIT failed(QStringLiteral("plumb open %1: %2").arg(m_portName, nine.errorString())); return; } static constexpr int PollMs = 200; bool pending = false; while (!m_stop.loadRelaxed()) { if (!pending) { if (!nine.beginRead(PortFid, 0, nine.msize())) { Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString())); break; } pending = true; } QByteArray buf; bool timedOut = false; const int n = nine.recvReadReply(&buf, PollMs, &timedOut); if (timedOut) continue; if (n < 0) { if (!m_stop.loadRelaxed()) Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString())); break; } pending = false; if (n == 0) continue; PlumbMsg m; if (PlumbMsg::unpack(buf, &m)) { Q_EMIT message(QString::fromUtf8(m.data), m.lookup(QStringLiteral("addr")), m.wdir); } } } private: QString m_socketPath; QString m_portName; QAtomicInt m_stop{0}; }; // --------------------------------------------------------------------------- // Plumber // --------------------------------------------------------------------------- Plumber::Plumber(QObject *parent) : QObject(parent) { } Plumber::~Plumber() { auto stopReader = [](PlumbReader *&reader) { if (!reader) return; reader->stop(); if (reader->wait(3000)) { delete reader; } else { QObject::connect(reader, &QThread::finished, reader, &QObject::deleteLater); } reader = nullptr; }; stopReader(m_reader); stopReader(m_ollieReader); } bool Plumber::available() const { const QString path = socketPath(); if (path.isEmpty()) return false; return QFileInfo::exists(path); } bool Plumber::send(const QString &data, const QString &wdir) { return sendTo(data, QString(), wdir); } bool Plumber::sendTo(const QString &data, const QString &dst, const QString &wdir) { if (data.isEmpty()) return false; const QString path = socketPath(); if (path.isEmpty()) { m_error = QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)"); Q_EMIT plumbError(m_error); return false; } NineP nine; if (!nine.connectAndAttach(path, currentUser())) { m_error = nine.errorString(); Q_EMIT plumbError(m_error); return false; } static constexpr uint32_t SendFid = 1; if (!nine.walk(SendFid, QStringLiteral("send")) || !nine.open(SendFid, NineP::OWRITE)) { m_error = nine.errorString(); Q_EMIT plumbError(m_error); return false; } PlumbMsg m; m.src = QStringLiteral("ollie"); m.dst = dst; m.wdir = wdir.isEmpty() ? QDir::currentPath() : wdir; m.type = QStringLiteral("text"); m.data = data.toUtf8(); const QByteArray packed = m.pack(); const int w = nine.write(SendFid, 0, packed); if (w != packed.size()) { m_error = nine.errorString().isEmpty() ? QStringLiteral("short plumb write (%1/%2)").arg(w).arg(packed.size()) : nine.errorString(); Q_EMIT plumbError(m_error); return false; } m_error.clear(); return true; } void Plumber::startReader() { const QString path = socketPath(); if (path.isEmpty()) { Q_EMIT readerError(QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)")); return; } // Start ollie port reader (for ollie:// URLs) if (!m_ollieReader) { m_ollieReader = new PlumbReader(path, QStringLiteral("ollie")); connect(m_ollieReader, &PlumbReader::message, this, [this](const QString &data, const QString &, const QString &) { Q_EMIT ollieMessage(data); }, Qt::QueuedConnection); connect(m_ollieReader, &PlumbReader::failed, this, [](const QString &msg) { // Silently ignore ollie port errors — port may not exist in plumb rules qDebug("plumber: ollie port: %s", qPrintable(msg)); }, Qt::QueuedConnection); m_ollieReader->start(); } } #include "plumber.moc"