diff --git a/.gitignore b/.gitignore index f147edf..fab7372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,73 @@ -# C++ objects and libs -*.slo -*.lo -*.o +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave *.a -*.la -*.lai +*.core +*.moc +*.o +*.obj +*.orig +*.rej *.so *.so.* -*.dll -*.dylib - -# Qt-es -object_script.*.Release -object_script.*.Debug -*_plugin_import.cpp +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc /.qmake.cache /.qmake.stash -*.pro.user -*.pro.user.* -*.qbs.user -*.qbs.user.* -*.moc -moc_*.cpp -moc_*.h -qrc_*.cpp -ui_*.h -*.qmlc -*.jsc -Makefile* -*build-* -*.qm -*.prl -# Qt unit tests -target_wrapper.* +# qtcreator generated files +*.pro.user* -# QtCreator -*.autosave +# xemacs temporary files +*.flc -# QtCreator Qml -*.qmlproject.user -*.qmlproject.user.* +# Vim temporary files +.*.swp -# QtCreator CMake -CMakeLists.txt.user* +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* -# QtCreator 4.8< compilation database -compile_commands.json +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe -# QtCreator local machine specific files for imported projects -*creator.user* diff --git a/avivpn.pro b/avivpn.pro new file mode 100644 index 0000000..474203b --- /dev/null +++ b/avivpn.pro @@ -0,0 +1,21 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 + +DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +SOURCES += \ + entry.cpp \ + main.cpp \ + mainwindow.cpp \ + vpnmodel.cpp + +HEADERS += \ + entry.h \ + mainwindow.h \ + vpnmodel.h + +FORMS += \ + mainwindow.ui diff --git a/entry.cpp b/entry.cpp new file mode 100644 index 0000000..174a788 --- /dev/null +++ b/entry.cpp @@ -0,0 +1,119 @@ +#include "entry.h" + +#include +#include + +Entry::Entry(const QString &name, const QString &tunnel, bool sudo, QObject *parent) : + QObject{parent}, m_name{name}, m_tunnel{tunnel}, m_sudo{sudo} +{ + connect(&m_process, &QProcess::errorOccurred, this, &Entry::errorOccurred); + connect(&m_process, &QProcess::stateChanged, this, &Entry::stateChanged); + connect(&m_process, &QProcess::readyReadStandardOutput, this, &Entry::readyReadStandardOutput); + connect(&m_process, &QProcess::readyReadStandardError, this, &Entry::readyReadStandardError); +} + +Entry::~Entry() +{ + qDebug() << m_name; + m_process.terminate(); + if (!m_process.waitForFinished(250)) + m_process.kill(); +} + +Qt::CheckState Entry::state() const +{ + switch (m_process.state()) + { + case QProcess::Starting: + return Qt::PartiallyChecked; + case QProcess::Running: + return Qt::Checked; + case QProcess::NotRunning: + return Qt::Unchecked; + } +} + +void Entry::toggle() +{ + if (m_process.state() == QProcess::NotRunning) + start(); + else + stop(); +} + +void Entry::start() +{ + qDebug() << m_name; + m_logOutput.clear(); + m_process.start(binaryName(), arguments()); +} + +void Entry::stop() +{ + qDebug() << m_name; + m_process.terminate(); +} + +QString Entry::binaryName() const +{ + return m_sudo ? "sudo" : "ssh"; +} + +QStringList Entry::arguments() const +{ + QStringList args { + "-v", // Verbose mode. Causes ssh to print debugging messages about its progress. + "-N", // Do not execute a remote command. This is useful for just forwarding ports. + "-T", // Disable pseudo-terminal allocation. + "-oServerAliveInterval=60", + "-oExitOnForwardFailure=yes", + m_tunnel, + "pc178" + }; + + if (m_sudo) + { + const QDir sshDir(QDir::home().absoluteFilePath(".ssh")); + + args.insert(0, "ssh"); + args.insert(1, "-F"); + args.insert(2, sshDir.absoluteFilePath("config")); + args.insert(3, "-i"); + args.insert(4, sshDir.absoluteFilePath("id_rsa")); + } + + return args; +} + +void Entry::errorOccurred(QProcess::ProcessError error) +{ + qDebug() << m_name << error; +} + +void Entry::stateChanged(QProcess::ProcessState state) +{ + qDebug() << m_name << state; + emit dataChanged(); +} + +void Entry::readyReadStandardOutput() +{ + m_process.setReadChannel(QProcess::StandardOutput); + while (m_process.canReadLine()) + { + const auto line = m_process.readLine(); + //qDebug() << m_name << line; + m_logOutput.append(line); + } +} + +void Entry::readyReadStandardError() +{ + m_process.setReadChannel(QProcess::StandardError); + while (m_process.canReadLine()) + { + const auto line = m_process.readLine(); + //qDebug() << m_name << line; + m_logOutput.append(line); + } +} diff --git a/entry.h b/entry.h new file mode 100644 index 0000000..3dc2a97 --- /dev/null +++ b/entry.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +class Entry : public QObject +{ + Q_OBJECT + +public: + Entry(const QString &name, const QString &tunnel, bool sudo, QObject *parent = nullptr); + ~Entry() override; + + const QString &name() const { return m_name; } + const QString &tunnel() const { return m_tunnel; } + bool sudo() const { return m_sudo; } + + const QString &logOutput() const { return m_logOutput; } + + Qt::CheckState state() const; + + void toggle(); + void start(); + void stop(); + + QString binaryName() const; + QStringList arguments() const; + +signals: + void dataChanged(); + +private slots: + void errorOccurred(QProcess::ProcessError error); + void stateChanged(QProcess::ProcessState state); + void readyReadStandardOutput(); + void readyReadStandardError(); + +private: + const QString m_name; + const QString m_tunnel; + const bool m_sudo; + + bool m_readyReceived{false}; + + QProcess m_process; + + QString m_logOutput; +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..8eeed02 --- /dev/null +++ b/main.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} " + "[" + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + "] " + "%{function}(): " + "%{message}")); + + MainWindow mainWindow; + mainWindow.show(); + + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..626494d --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,46 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow{parent}, + m_ui{std::make_unique()} +{ + m_ui->setupUi(this); + + m_ui->treeView->setModel(&m_model); + + connect(m_ui->treeView, &QWidget::customContextMenuRequested, [&view=*m_ui->treeView,&model=m_model](const QPoint &pos){ + const auto index = view.indexAt(pos); + if (!index.isValid()) + return; + + const auto &entry = model.getEntry(index); + + QMenu menu; + const auto showLogAction = menu.addAction("Show log"); + const auto selectedAction = menu.exec(view.viewport()->mapToGlobal(pos)); + + if (selectedAction == showLogAction) + { + QDialog dialog; + QVBoxLayout layout; + QPlainTextEdit widget; + QFont font = widget.document()->defaultFont(); + font.setFamily("Courier New"); + widget.document()->setDefaultFont(font); + widget.setLineWrapMode(QPlainTextEdit::NoWrap); + widget.setReadOnly(true); + widget.setPlainText(entry.logOutput()); + layout.addWidget(&widget); + dialog.setLayout(&layout); + dialog.exec(); + } + }); +} + +MainWindow::~MainWindow() = default; diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..e9e6028 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +#include "vpnmodel.h" + +namespace Ui { class MainWindow; } + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private: + std::unique_ptr m_ui; + + VpnModel m_model; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..f56a7c0 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,63 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Start all + + + + + + + Stop all + + + + + + + + + Qt::CustomContextMenu + + + false + + + + + + + + + diff --git a/vpnmodel.cpp b/vpnmodel.cpp new file mode 100644 index 0000000..f30c987 --- /dev/null +++ b/vpnmodel.cpp @@ -0,0 +1,142 @@ +#include "vpnmodel.h" + +#include +#include + +#include + +namespace { +enum { + ColumnName, + ColumnCommand, + NumberOfColumns +}; +} + +VpnModel::VpnModel(QObject *parent) : + QAbstractTableModel{parent} +{ + for (Entry &entry : m_entries) + connect(&entry, &Entry::dataChanged, this, &VpnModel::entryChanged); +} + +int VpnModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return std::size(m_entries); +} + +int VpnModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return NumberOfColumns; +} + +QVariant VpnModel::data(const QModelIndex &index, int role) const +{ + const auto &entry = m_entries[index.row()]; + + switch (index.column()) + { + case ColumnName: + switch (Qt::ItemDataRole(role)) + { + case Qt::DisplayRole: + case Qt::EditRole: + return entry.name(); + case Qt::CheckStateRole: + return entry.state(); + case Qt::FontRole: + QFont font; + font.setBold(true); + return font; + } + break; + case ColumnCommand: + switch (Qt::ItemDataRole(role)) + { + case Qt::DisplayRole: + case Qt::EditRole: + return entry.binaryName() + ' ' + entry.arguments().join(' '); + } + break; + } + + return {}; +} + +QVariant VpnModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (orientation) + { + case Qt::Horizontal: + switch (section) + { + case ColumnName: + switch (Qt::ItemDataRole(role)) + { + case Qt::DisplayRole: + case Qt::EditRole: + return tr("Name"); + } + break; + case ColumnCommand: + switch (Qt::ItemDataRole(role)) + { + case Qt::DisplayRole: + case Qt::EditRole: + return tr("Command"); + } + break; + } + break; + } + + return {}; +} + +bool VpnModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + auto &entry = m_entries[index.row()]; + + switch (index.column()) + { + case ColumnName: + switch (role) + { + case Qt::CheckStateRole: + entry.toggle(); + break; + } + break; + } + + return false; +} + +Qt::ItemFlags VpnModel::flags(const QModelIndex &index) const +{ + auto flags = QAbstractTableModel::flags(index); + if (index.column() == 0) + flags |= Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate; + return flags; +} + +void VpnModel::entryChanged() +{ + const auto *sender_ptr = sender(); + const auto iter = std::find_if(std::cbegin(m_entries), std::cend(m_entries), [sender_ptr](const Entry &entry){ + return &entry == sender_ptr; + }); + + if (iter == std::cend(m_entries)) + { + qCritical() << "unknown sender" << sender_ptr; + return; + } + + const auto row = std::distance(std::cbegin(m_entries), iter); + + const auto index = createIndex(row, 0); + emit dataChanged(index, index, { Qt::CheckStateRole }); +} diff --git a/vpnmodel.h b/vpnmodel.h new file mode 100644 index 0000000..83b74b2 --- /dev/null +++ b/vpnmodel.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +#include "entry.h" + +class VpnModel : public QAbstractTableModel +{ + Q_OBJECT +public: + VpnModel(QObject *parent = nullptr); + + Entry &getEntry(const QModelIndex &index) { return getEntry(index.row()); } + const Entry &getEntry(const QModelIndex &index) const { return getEntry(index.row()); } + + Entry &getEntry(int row) { return m_entries[row]; } + const Entry &getEntry(int row) const { return m_entries[row]; } + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + +private slots: + void entryChanged(); + +private: + std::array m_entries { + Entry { "Bamboo", "-L 127.1.0.1:2233:bamboo.avibit.com:2233", false }, + Entry { "Bitbucket", "-L 127.2.0.1:2233:bitbucket.avibit.com:2233", false }, + Entry { "Crucible", "-L 127.3.0.1:2233:crucible.avibit.com:2233", false }, + Entry { "Confluence", "-L 127.4.0.1:2233:confluence.avibit.com:2233", false }, + Entry { "Jira", "-L 127.5.0.1:2233:jira.avibit.com:2233", false }, + + Entry { "SVN", "-L 3690:svn.avibit.com:3690", false }, + Entry { "Bitbucket SSH", "-L 127.2.0.1:7999:bitbucket.avibit.com:7999", false }, + Entry { "Timetool", "-L 8080:timetool.avibit.com:8080", false }, + //Entry { "Mirror", "-L 80:mirror.avibit.com:80", true }, + Entry { "Intranet", "-L 80:intranet.avibit.com:80", true }, + Entry { "Sonarqube", "-L 9000:localhost:9000", false } + }; +};