ollie/kde/gui/plumber.cpp

231 lines
6.6 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "plumber.h"
#include "ninep.h"
#include "plumbmsg.h"
#include <QByteArray>
#include <QDir>
#include <QFileInfo>
#include <QThread>
#include <unistd.h>
// 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 the edit 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, QObject *parent = nullptr)
: QThread(parent)
, m_socketPath(socketPath)
{
}
void stop()
{
m_stop.storeRelaxed(1);
}
Q_SIGNALS:
void message(const QString &file, 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 EditFid = 1;
if (!nine.walk(EditFid, QStringLiteral("edit")) || !nine.open(EditFid, NineP::OREAD)) {
Q_EMIT failed(QStringLiteral("plumb open edit: %1").arg(nine.errorString()));
return;
}
static constexpr int PollMs = 200;
bool pending = false;
while (!m_stop.loadRelaxed()) {
if (!pending) {
if (!nine.beginRead(EditFid, 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;
QAtomicInt m_stop{0};
};
// ---------------------------------------------------------------------------
// Plumber
// ---------------------------------------------------------------------------
Plumber::Plumber(QObject *parent)
: QObject(parent)
{
}
Plumber::~Plumber()
{
if (!m_reader)
return;
m_reader->stop();
if (m_reader->wait(3000)) {
delete m_reader;
} else {
connect(m_reader, &QThread::finished, m_reader, &QObject::deleteLater);
}
m_reader = nullptr;
}
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()
{
if (m_reader)
return;
const QString path = socketPath();
if (path.isEmpty()) {
Q_EMIT readerError(QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)"));
return;
}
m_reader = new PlumbReader(path);
connect(m_reader, &PlumbReader::message, this, &Plumber::edit, Qt::QueuedConnection);
connect(m_reader, &PlumbReader::failed, this, &Plumber::readerError, Qt::QueuedConnection);
m_reader->start();
}
#include "plumber.moc"