From 9908dffbb3c7c6efedb51aee95263726bb42ac60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Gren=C3=A4ngen?= Date: Sat, 6 Jul 2024 14:54:39 +0200 Subject: [PATCH] Initial add --- .gitignore | 2 + CMakeLists.txt | 57 +++++++++++++++++ MainWindow.cpp | 65 +++++++++++++++++++ MainWindow.h | 43 +++++++++++++ MainWindow.ui | 37 +++++++++++ TailRunner.cpp | 112 +++++++++++++++++++++++++++++++++ TailRunner.h | 44 +++++++++++++ TrayMenuManager.cpp | 141 +++++++++++++++++++++++++++++++++++++++++ TrayMenuManager.h | 46 ++++++++++++++ icons/tray-off.png | Bin 0 -> 8660 bytes icons/tray-on.png | Bin 0 -> 10686 bytes main.cpp | 12 ++++ models.h | 149 ++++++++++++++++++++++++++++++++++++++++++++ resources.qrc | 6 ++ 14 files changed, 714 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 MainWindow.cpp create mode 100644 MainWindow.h create mode 100644 MainWindow.ui create mode 100644 TailRunner.cpp create mode 100644 TailRunner.h create mode 100644 TrayMenuManager.cpp create mode 100644 TrayMenuManager.h create mode 100644 icons/tray-off.png create mode 100644 icons/tray-on.png create mode 100644 main.cpp create mode 100644 models.h create mode 100644 resources.qrc diff --git a/.gitignore b/.gitignore index 46f42f8..85fb24c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps + +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7e21c0c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 3.5) + +project(tail-tray VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +set(PROJECT_SOURCES + main.cpp + MainWindow.cpp + MainWindow.h + MainWindow.ui + resources.qrc + models.h + TrayMenuManager.cpp + TrayMenuManager.h +) + +qt_add_executable(tail-tray + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + TailRunner.h TailRunner.cpp +) + +target_link_libraries(tail-tray PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manually though. +if(${QT_VERSION} VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.tail-tray) +endif() +set_target_properties(tail-tray PROPERTIES + ${BUNDLE_ID_OPTION} + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +include(GNUInstallDirs) +install(TARGETS tail-tray + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(tail-tray) +endif() diff --git a/MainWindow.cpp b/MainWindow.cpp new file mode 100644 index 0000000..20eb5ce --- /dev/null +++ b/MainWindow.cpp @@ -0,0 +1,65 @@ +#include "MainWindow.h" +#include "./ui_MainWindow.h" + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) + , pTrayManager(nullptr) + , eCurrentState(TailState::NoAccount) + , pCurrentExecution(nullptr) + , pTailStatus(nullptr) + , pStatusCheckTimer(nullptr) +{ + ui->setupUi(this); + + pStatusCheckTimer = new QTimer(this); + connect(pStatusCheckTimer, &QTimer::timeout, this, [this]() { + pCurrentExecution->checkStatus(); + }); + pStatusCheckTimer->setSingleShot(false); + pStatusCheckTimer->start(1000 * 30); // 30sec interval + + pCurrentExecution = new TailRunner(this); + connect(pCurrentExecution, &TailRunner::statusUpdated, this, &MainWindow::onTailStatusChanged); + + pTrayManager = new TrayMenuManager(pCurrentExecution, this); + + changeToState(TailState::NotLoggedIn); + pCurrentExecution->checkStatus(); +} + +MainWindow::~MainWindow() +{ + pStatusCheckTimer->stop(); + delete pStatusCheckTimer; + + delete pCurrentExecution; + delete pTailStatus; + delete pTrayManager; + delete ui; +} + +TailState MainWindow::changeToState(TailState newState) +{ + auto retVal = eCurrentState; + eCurrentState = newState; + + pTrayManager->stateChangedTo(newState, pTailStatus); + + return retVal; +} + +void MainWindow::onTailStatusChanged(TailStatus* pNewStatus) +{ + delete pTailStatus; + + pTailStatus = pNewStatus; + if (pTailStatus->user->id > 0) + { + // Logged in + if (pTailStatus->health.count() < 1) + changeToState(TailState::Connected); + else + changeToState(TailState::NotConnected); + } +} diff --git a/MainWindow.h b/MainWindow.h new file mode 100644 index 0000000..ae1731a --- /dev/null +++ b/MainWindow.h @@ -0,0 +1,43 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include + +#include "TailRunner.h" +#include "TrayMenuManager.h" + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow* ui; + + TrayMenuManager* pTrayManager; + + TailState eCurrentState; + TailRunner* pCurrentExecution; + TailStatus* pTailStatus; + QTimer* pStatusCheckTimer; + + +private: + // Switch to the new state and return the prev (old) state back to caller + TailState changeToState(TailState newState); + + void onTailStatusChanged(TailStatus* pNewStatus); +}; +#endif // MAINWINDOW_H diff --git a/MainWindow.ui b/MainWindow.ui new file mode 100644 index 0000000..27ba41d --- /dev/null +++ b/MainWindow.ui @@ -0,0 +1,37 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + :/icons/tray-off.png:/icons/tray-off.png + + + + + + 0 + 0 + 800 + 30 + + + + + + + + + + diff --git a/TailRunner.cpp b/TailRunner.cpp new file mode 100644 index 0000000..6d8a74d --- /dev/null +++ b/TailRunner.cpp @@ -0,0 +1,112 @@ +#include "TailRunner.h" + +#include +#include +#include +#include +#include + +TailRunner::TailRunner(QObject* parent) + : QObject(parent) + , pProcess(nullptr) + , eCommand(Command::Status) { +} + +TailRunner::~TailRunner() +{ + delete pProcess; +} + +void TailRunner::checkStatus() +{ + eCommand = Command::Status; + runCommand("status", QStringList(), true); +} + +void TailRunner::start() +{ + // tailscale up --operator marcus --accept-routes --exit-node pelican + eCommand = Command::Connect; + QStringList args; + args << "--operator" << "marcus"; + args << "--accept-routes"; + args << "--exit-node" << "pelican"; + + runCommand("up", args, true); +} + +void TailRunner::stop() +{ + eCommand = Command::Disconnect; + runCommand("down", QStringList()); +} + +void TailRunner::runCommand(QString cmd, QStringList args, bool jsonResult) +{ + if (pProcess != nullptr) + { + if (pProcess->state() == QProcess::Running) { + assert(!"Process already running!"); + return; + } + + delete pProcess; + } + + pProcess = new QProcess(this); + connect(pProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) { + qDebug() << "Failed to run process: " << err; + }); + + connect(pProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) { + qDebug() << "Process exited with " << exitCode << " - " << status; + + // After we've invoked a command not status command we check for new status update + if (eCommand != Command::Status) { + checkStatus(); + } + }); + + connect(pProcess, &QProcess::readyReadStandardOutput, + this, &TailRunner::onProcessCanReadStdOut); + + if (jsonResult) + args << "--json"; + + args.insert(0, cmd); + pProcess->start("tailscale", args); +} + +void TailRunner::onProcessCanReadStdOut() { + assert(pProcess != nullptr); + + auto data = pProcess->readAllStandardOutput(); + + // Parse the status object + + switch (eCommand) { + case Command::Status: { + QJsonParseError* parseError = nullptr; + QJsonDocument doc = QJsonDocument::fromJson(data, parseError); + + if (parseError != nullptr) + { + qDebug() << parseError->errorString(); + return; + } + QJsonObject obj = doc.object(); + parseStatusResponse(obj); + break; + } + case Command::Connect: { + break; + } + case Command::Disconnect: { + break; + } + } +} + +void TailRunner::parseStatusResponse(const QJsonObject& obj) { + emit statusUpdated(TailStatus::parse(obj)); +} diff --git a/TailRunner.h b/TailRunner.h new file mode 100644 index 0000000..38d62b2 --- /dev/null +++ b/TailRunner.h @@ -0,0 +1,44 @@ +#ifndef TAILRUNNER_H +#define TAILRUNNER_H + +#include +#include +#include +#include +#include + +#include "models.h" + +class TailRunner : public QObject +{ + Q_OBJECT +public: + explicit TailRunner(QObject* parent = nullptr); + virtual ~TailRunner(); + + void checkStatus(); + + void start(); + void stop(); + +private: + QProcess* pProcess; + enum class Command { + Connect, + Disconnect, + Status + }; + + Command eCommand; + +signals: + void statusUpdated(TailStatus* newStatus); + +private: + void runCommand(QString cmd, QStringList args, bool jsonResult = false); + void onProcessCanReadStdOut(); + + void parseStatusResponse(const QJsonObject& obj); +}; + +#endif // TAILRUNNER_H diff --git a/TrayMenuManager.cpp b/TrayMenuManager.cpp new file mode 100644 index 0000000..e08c8da --- /dev/null +++ b/TrayMenuManager.cpp @@ -0,0 +1,141 @@ +// +// Created by marcus on 2024-07-06. +// + +#include + +#include "TrayMenuManager.h" + +TrayMenuManager::TrayMenuManager(TailRunner* runner, QObject* parent) + : QObject(parent) + , pTailRunner(runner) + , pSysTray(nullptr) + , pTrayMenu(nullptr) + , pConnect(nullptr) + , pDisconnect(nullptr) + , pQuitAction(nullptr) + , pLoginAction(nullptr) + , pConnected(nullptr) + , pLogoutAction(nullptr) + , pPreferences(nullptr) + , pAbout(nullptr) +{ + pTrayMenu = new QMenu("Tailscale"); + pSysTray = new QSystemTrayIcon(this); + pSysTray->setContextMenu(pTrayMenu); + pSysTray->setToolTip("Tailscale"); + pSysTray->setIcon(QIcon(":/icons/tray-off.png")); + pSysTray->setVisible(true); + + pQuitAction = new QAction("Quit"); + pLoginAction = new QAction("Login"); + pLogoutAction = new QAction("Logout"); + pPreferences = new QAction("Preferences"); + pAbout = new QAction("About..."); + pConnected = new QAction("Connected"); + pConnected->setEnabled(false); + pConnect = new QAction("Connect"); + pDisconnect = new QAction("Disconnect"); + + connect(pConnect, &QAction::triggered, this, [this](bool) { + pTailRunner->start(); + }); + + connect(pDisconnect, &QAction::triggered, this, [this](bool) { + pTailRunner->stop(); + }); + + connect(pQuitAction, &QAction::triggered, qApp, &QApplication::quit); + + stateChangedTo(TailState::NotLoggedIn, nullptr); +} + +TrayMenuManager::~TrayMenuManager() +{ + delete pTrayMenu; + delete pSysTray; + delete pConnect; + delete pDisconnect; + delete pQuitAction; + delete pLoginAction; + delete pConnected; + delete pLogoutAction; + delete pPreferences; + delete pAbout; +} + +void TrayMenuManager::stateChangedTo(TailState newState, TailStatus const* pTailStatus) +{ + switch (newState) { + case TailState::Connected: + case TailState::LoggedIn: + buildConnectedMenu(pTailStatus); + break; + case TailState::NoAccount: + case TailState::NotLoggedIn: + buildNotLoggedInMenu(); + break; + case TailState::NotConnected: + buildNotConnectedMenu(pTailStatus); + break; + case TailState::ConnectedWithExitNode: + buildConnectedExitNodeMenu(pTailStatus); + break; + default: + assert(!"Unhandled TailState status!"); + } +} + +void TrayMenuManager::buildNotLoggedInMenu() +{ + pTrayMenu->clear(); + pTrayMenu->addAction(pLoginAction); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pPreferences); + pTrayMenu->addAction(pAbout); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pQuitAction); + + pSysTray->setIcon(QIcon(":/icons/tray-off.png")); +} + +void TrayMenuManager::buildNotConnectedMenu(TailStatus const* pTailStatus) +{ + pTrayMenu->clear(); + pTrayMenu->addAction(pConnect); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pTailStatus->user->loginName); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pPreferences); + pTrayMenu->addAction(pAbout); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pQuitAction); + + pSysTray->setIcon(QIcon(":/icons/tray-off.png")); +} + +void TrayMenuManager::buildConnectedMenu(TailStatus const* pTailStatus) +{ + pTrayMenu->clear(); + pTrayMenu->addAction(pConnected); + pTrayMenu->addAction(pDisconnect); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pTailStatus->user->loginName); + pTrayMenu->addSeparator(); + pTrayMenu->addAction("This device: " + pTailStatus->self->hostName); + pTrayMenu->addAction("Network devices"); + pTrayMenu->addSeparator(); + pTrayMenu->addAction("Exit node (none)"); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pPreferences); + pTrayMenu->addAction(pAbout); + pTrayMenu->addSeparator(); + pTrayMenu->addAction(pQuitAction); + + pSysTray->setIcon(QIcon(":/icons/tray-on.png")); +} + +void TrayMenuManager::buildConnectedExitNodeMenu(TailStatus const* pTailStatus) +{ + +} \ No newline at end of file diff --git a/TrayMenuManager.h b/TrayMenuManager.h new file mode 100644 index 0000000..2b168a5 --- /dev/null +++ b/TrayMenuManager.h @@ -0,0 +1,46 @@ +// +// Created by marcus on 2024-07-06. +// + +#ifndef TRAYMENUMANAGER_H +#define TRAYMENUMANAGER_H + +#include +#include +#include + +#include "models.h" +#include "TailRunner.h" + +class TrayMenuManager : public QObject +{ + Q_OBJECT +public: + explicit TrayMenuManager(TailRunner* runner, QObject* parent = nullptr); + virtual ~TrayMenuManager(); + + void stateChangedTo(TailState newState, TailStatus const* pTailStatus); + +private: + TailRunner* pTailRunner; + QSystemTrayIcon* pSysTray; + QMenu* pTrayMenu; + QAction* pQuitAction; + QAction* pLoginAction; + QAction* pLogoutAction; + QAction* pConnected; + QAction* pConnect; + QAction* pDisconnect; + QAction* pPreferences; + QAction* pAbout; + +private: + void buildNotLoggedInMenu(); + void buildNotConnectedMenu(TailStatus const* pTailStatus); + void buildConnectedMenu(TailStatus const* pTailStatus); + void buildConnectedExitNodeMenu(TailStatus const* pTailStatus); +}; + + + +#endif //TRAYMENUMANAGER_H diff --git a/icons/tray-off.png b/icons/tray-off.png new file mode 100644 index 0000000000000000000000000000000000000000..3e4b12507387bc37c1ccab97dfdb96164263552b GIT binary patch literal 8660 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 W>eAZw)95D#fQq7qLYbWToBse@xIUQx literal 0 HcmV?d00001 diff --git a/icons/tray-on.png b/icons/tray-on.png new file mode 100644 index 0000000000000000000000000000000000000000..bfdaea7be855001d76b68cee20867a0c3ad07410 GIT binary patch literal 10686 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 literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..409e9e1 --- /dev/null +++ b/main.cpp @@ -0,0 +1,12 @@ +#include "MainWindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.hide(); + + return a.exec(); +} diff --git a/models.h b/models.h new file mode 100644 index 0000000..88aeca3 --- /dev/null +++ b/models.h @@ -0,0 +1,149 @@ +// +// Created by marcus on 2024-07-05. +// + +#ifndef MODELS_H +#define MODELS_H + +#include +#include +#include +#include +#include +#include + +enum class TailState { + NoAccount, + NotLoggedIn, + LoggedIn, + NotConnected, + Connected, + ConnectedWithExitNode +}; + +class TailDeviceInfo : public QObject +{ + Q_OBJECT +public: + QString id; + QString publicKey; + QString hostName; + QString dnsName; + QString os; + long long userId; + QList tailscaleIPs; + QList allowedIPs; + QList addrs; + QString curAddr; + QString relay; + bool online; + bool exitNode; + bool exitNodeOption; + bool active; + QList peerApiUrl; + QList capabilities; + QList capMap; + bool inNetworkMap; + bool inMagicSock; + bool inEngine; + QDateTime keyExpiry; + + static TailDeviceInfo* parse(const QJsonObject& obj) + { + auto self = new TailDeviceInfo{}; + + self->id = obj["ID"].toString(""); + self->publicKey = obj["PublicKey"].toString(""); + self->hostName = obj["HostName"].toString(""); + self->dnsName = obj["DNSName"].toString(""); + self->os = obj["OS"].toString(""); + self->userId = obj["UserID"].toInteger(); + for (const auto& ab : obj["TailscaleIPs"].toArray()) + self->tailscaleIPs.emplace_back(ab.toString("")); + + return self; + } +}; + +class TailUser : public QObject +{ + Q_OBJECT +public: + long long id; + QString loginName; + QString displayName; + QString profilePicUrl; + QList roles; + + static TailUser* parse(const QJsonObject& obj, long long userId) { + auto user = new TailUser{}; + + // Get the user object based on the user id (user id comes from the self part in the same doc) + auto thisUser = obj[QString::number(userId)].toObject(); + + user->id = thisUser["ID"].toInteger(0L); + user->loginName = thisUser["LoginName"].toString(""); + user->displayName = thisUser["DisplayName"].toString(""); + user->profilePicUrl = thisUser["ProfilePicURL"].toString(""); + for (const auto& ab : thisUser["roles"].toArray()) + user->roles.emplace_back(ab.toString("")); + + return user; + } +}; + +class TailStatus : public QObject +{ + Q_OBJECT +public: + QString version; + bool tun; + QString backendState; + bool haveNodeKey; + QString authUrl; + QList tailscaleIPs; + TailDeviceInfo* self; + QList health; + QString magicDnsSuffix; + QObject qurrentTailNet; + QList certDomains; + QList peers; + TailUser* user; + QString clientVersion; + + virtual ~TailStatus() + { + delete self; + delete user; + } + + static TailStatus* parse(const QJsonObject& obj) { + auto newStatus = new TailStatus{}; + newStatus->version = obj["Version"].toString(""); + newStatus->tun = obj["TUN"].toBool(false); + newStatus->backendState = obj["backendState"].toString(""); + newStatus->haveNodeKey = obj["HaveNodeKey"].toBool(false); + newStatus->authUrl = obj["AuthURL"].toString(""); + for (const auto& ab : obj["TailscaleIPs"].toArray()) + newStatus->tailscaleIPs.emplace_back(ab.toString("")); + + if (!obj["Health"].isNull()) + { + for (const auto& ab : obj["Health"].toArray()) + newStatus->health.emplace_back(ab.toString("")); + } + newStatus->magicDnsSuffix = obj["MagicDNSSuffix"].toString(""); + + for (const auto& ab : obj["CertDomains"].toArray()) + newStatus->certDomains.emplace_back(ab.toString("")); + + newStatus->clientVersion = obj["ClientVersion"].toString(""); + + newStatus->self = TailDeviceInfo::parse(obj["Self"].toObject()); + newStatus->user = TailUser::parse(obj["User"].toObject(), newStatus->self->userId); + + return newStatus; + } +}; + +#endif //MODELS_H diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..68aa1da --- /dev/null +++ b/resources.qrc @@ -0,0 +1,6 @@ + + + icons/tray-on.png + icons/tray-off.png + +