231 lines
6.6 KiB
C++
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"
|