syncthingtray/cli/application.cpp
Martchus 239f750028 Improve timestamp parsing/handling
* Use local time consistently for timestamps which might get displayed
  within the UI, e.g. the dir status was in one case set to the local time
  and in other cases GMT was used which could lead to discarding status
  updates
* Print warning when timestamp parsing fails (instead of ignoring it
  silently)
* Set directory status from folder status response even when parsing the
  timestamp fails
* Catch possible exception when printing log timestamps in syncthingctl
2021-05-18 00:08:21 +02:00

1146 lines
45 KiB
C++

#include "./application.h"
#include "./helper.h"
#include "./jsconsole.h"
#include "./jsdefs.h"
#include "./jsincludes.h"
#include <syncthingconnector/syncthingconfig.h>
#include <syncthingconnector/utils.h>
// use header-only functions waitForSignals() and signalInfo() from test utilities; disable assertions via macro
#define SYNCTHINGTESTHELPER_FOR_CLI
#include "../testhelper/helper.h"
#include "resources/config.h"
#include <c++utilities/chrono/timespan.h>
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <c++utilities/misc/parseerror.h>
#include <qtutilities/misc/conversion.h>
#include <QCoreApplication>
#include <QDir>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QStringBuilder>
#include <QTemporaryFile>
#include <QTimer>
#include <functional>
#include <iostream>
using namespace std;
using namespace std::placeholders;
using namespace CppUtilities;
using namespace CppUtilities::EscapeCodes;
using namespace QtUtilities;
using namespace Data;
namespace Cli {
static bool terminated = false;
static int statusCode = 0;
void exitApplication(int statusCode)
{
::Cli::statusCode = statusCode;
terminated = true;
}
inline QString argToQString(const char *arg, int size = -1)
{
#if !defined(PLATFORM_WINDOWS)
return QString::fromLocal8Bit(arg, size);
#else
// under Windows args are converted to UTF-8
return QString::fromUtf8(arg, size);
#endif
}
Application::Application()
: m_expectedResponse(0)
, m_preventDisconnect(false)
, m_callbacksInvoked(false)
, m_requiresMainEventLoop(true)
, m_idleDuration(0)
, m_idleTimeout(0)
, m_argsRead(false)
{
// take ownership over the global QNetworkAccessManager
networkAccessManager().setParent(this);
// setup callbacks
m_args.parser.setExitFunction(&exitApplication);
m_args.status.setCallback(bind(&Application::printStatus, this, _1));
m_args.log.setCallback(bind(&Application::requestLog, this, _1));
m_args.stop.setCallback(bind(&Application::requestShutdown, this, _1));
m_args.restart.setCallback(bind(&Application::requestRestart, this, _1));
m_args.rescan.setCallback(bind(&Application::requestRescan, this, _1));
m_args.rescanAll.setCallback(bind(&Application::requestRescanAll, this, _1));
m_args.pause.setCallback(bind(&Application::requestPauseResume, this, true));
m_args.resume.setCallback(bind(&Application::requestPauseResume, this, false));
m_args.waitForIdle.setCallback(bind(&Application::waitForIdle, this, _1));
m_args.pwd.setCallback(bind(&Application::checkPwdOperationPresent, this, _1));
m_args.cat.setCallback(bind(&Application::printConfig, this, _1));
m_args.edit.setCallback(bind(&Application::editConfig, this, _1));
m_args.statusPwd.setCallback(bind(&Application::printPwdStatus, this, _1));
m_args.rescanPwd.setCallback(bind(&Application::requestRescanPwd, this, _1));
m_args.pausePwd.setCallback(bind(&Application::requestPausePwd, this, _1));
m_args.resumePwd.setCallback(bind(&Application::requestResumePwd, this, _1));
m_args.dir.setCallback(bind(&Application::initDirCompletion, this, ref(m_args.dir), _1));
m_args.dev.setCallback(bind(&Application::initDevCompletion, this, ref(m_args.dev), _1));
// connect signals and slots
connect(&m_connection, &SyncthingConnection::statusChanged, this, &Application::handleStatusChanged);
connect(&m_connection, &SyncthingConnection::error, this, &Application::handleError);
}
Application::~Application()
{
}
int Application::exec(int argc, const char *const *argv)
{
try {
// parse arguments
m_args.parser.readArgs(argc, argv);
// check whether application needs to be terminated due to --bash-completion argument
if (terminated) {
return statusCode;
}
m_args.parser.checkConstraints();
m_argsRead = true;
} catch (const ParseError &failure) {
cerr << failure;
return 1;
}
// handle help argument
if (m_args.parser.helpArg().isPresent()) {
m_args.parser.printHelp(cout);
return 0;
}
// load configuration
if (const int res = loadConfig()) {
return res;
}
// finally do the request or establish connection
if (m_args.status.isPresent() || m_args.rescan.isPresent() || m_args.rescanAll.isPresent() || m_args.pause.isPresent()
|| m_args.resume.isPresent() || m_args.waitForIdle.isPresent() || m_args.pwd.isPresent()) {
// those arguments require establishing a connection first, the actual handler is called by handleStatusChanged() when
// the connection has been established
m_connection.reconnect(m_settings);
cerr << Phrases::Info << "Connecting to " << m_settings.syncthingUrl.toLocal8Bit().data() << " ..." << TextAttribute::Reset << flush;
} else {
// call handler for any other arguments directly
m_connection.applySettings(m_settings);
m_args.parser.invokeCallbacks();
}
// enter main event loop
if (!m_requiresMainEventLoop) {
return 0;
}
return QCoreApplication::exec();
}
int assignIntegerFromArg(const Argument &arg, int &integer)
{
if (!arg.isPresent()) {
return 0;
}
try {
integer = stringToNumber<int>(arg.firstValue());
if (integer < 0) {
throw ConversionException();
}
} catch (const ConversionException &) {
cerr << Phrases::Error << "The specified number of milliseconds \"" << arg.firstValue() << "\" is no unsigned integer." << Phrases::EndFlush;
return -4;
}
return 0;
}
int Application::loadConfig()
{
// locate and read Syncthing config file
QString configFile;
const char *configFileArgValue = m_args.configFile.firstValue();
if (configFileArgValue) {
configFile = fromNativeFileName(configFileArgValue);
} else {
configFile = SyncthingConfig::locateConfigFile();
}
SyncthingConfig config;
const char *apiKeyArgValue = m_args.apiKey.firstValue();
if (!config.restore(configFile)) {
if (configFileArgValue) {
cerr << Phrases::Error << "Unable to locate specified Syncthing config file \"" << configFileArgValue << "\"" << Phrases::EndFlush;
return -1;
} else if (!apiKeyArgValue) {
cerr << Phrases::Error << "Unable to locate Syncthing config file and no API key specified" << Phrases::EndFlush;
return -2;
}
}
// apply settings for connection
if (const char *urlArgValue = m_args.url.firstValue()) {
m_settings.syncthingUrl = argToQString(urlArgValue);
} else if (!config.guiAddress.isEmpty()) {
m_settings.syncthingUrl = config.syncthingUrl();
} else {
m_settings.syncthingUrl = QStringLiteral("http://localhost:8080");
}
if (m_args.credentials.isPresent()) {
m_settings.authEnabled = true;
m_settings.userName = argToQString(m_args.credentials.values(0)[0]);
m_settings.password = argToQString(m_args.credentials.values(0)[1]);
}
if (apiKeyArgValue) {
m_settings.apiKey.append(apiKeyArgValue);
} else {
m_settings.apiKey.append(config.guiApiKey.toUtf8());
}
if (const char *certArgValue = m_args.certificate.firstValue()) {
m_settings.httpsCertPath = argToQString(certArgValue);
if (m_settings.httpsCertPath.isEmpty() || !m_settings.loadHttpsCert()) {
cerr << Phrases::Error << "Unable to load specified certificate \"" << m_args.certificate.firstValue() << '\"' << Phrases::End << flush;
return -3;
}
}
// read idle duration and timeout
if (const int res = assignIntegerFromArg(m_args.atLeast, m_idleDuration)) {
return res;
}
if (const int res = assignIntegerFromArg(m_args.timeout, m_idleTimeout)) {
return res;
}
// disable polling for information which is not used by any CLI operation so far
m_settings.trafficPollInterval = 0;
m_settings.devStatsPollInterval = 0;
m_settings.errorsPollInterval = 0;
return 0;
}
bool Application::waitForConnected(int timeout)
{
bool isConnected = m_connection.isConnected();
const function<void(SyncthingStatus)> checkStatus([this, &isConnected](SyncthingStatus) { isConnected = m_connection.isConnected(); });
return waitForSignalsOrFail(bind(static_cast<void (SyncthingConnection::*)(SyncthingConnectionSettings &)>(&SyncthingConnection::reconnect),
ref(m_connection), ref(m_settings)),
timeout, signalInfo(&m_connection, &SyncthingConnection::error),
signalInfo(&m_connection, &SyncthingConnection::statusChanged, checkStatus, &isConnected));
}
bool Application::waitForConfig(int timeout)
{
m_connection.applySettings(m_settings);
return waitForSignalsOrFail(bind(&SyncthingConnection::requestConfig, ref(m_connection)), timeout,
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfig),
signalInfo(&m_connection, &SyncthingConnection::newDirs), signalInfo(&m_connection, &SyncthingConnection::newDevices));
}
bool Application::waitForConfigAndStatus(int timeout)
{
m_connection.applySettings(m_settings);
return waitForSignalsOrFail(bind(&SyncthingConnection::requestConfigAndStatus, ref(m_connection)), timeout,
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfig),
signalInfo(&m_connection, &SyncthingConnection::newDirs), signalInfo(&m_connection, &SyncthingConnection::newDevices),
signalInfo(&m_connection, &SyncthingConnection::myIdChanged));
}
void Application::handleStatusChanged(SyncthingStatus newStatus)
{
Q_UNUSED(newStatus)
// skip when callbacks have already been invoked, when doing shell completion or not connected yet
if (!m_argsRead || m_callbacksInvoked || !m_connection.isConnected()) {
return;
}
// erase current line
cerr << Phrases::Override;
// invoke callbacks
m_callbacksInvoked = true;
m_args.parser.invokeCallbacks();
// disconnect, except when m_preventDisconnect has been set in callbacks
if (!m_preventDisconnect) {
m_connection.disconnect();
}
}
void Application::handleResponse()
{
if (!m_expectedResponse) {
cerr << Phrases::Error << "Unexpected response" << Phrases::End << flush;
QCoreApplication::exit(-4);
return;
}
if (!--m_expectedResponse) {
QCoreApplication::quit();
}
}
void Application::handleError(
const QString &message, SyncthingErrorCategory category, int networkError, const QNetworkRequest &request, const QByteArray &response)
{
CPP_UTILITIES_UNUSED(category)
CPP_UTILITIES_UNUSED(networkError)
// skip error handling for shell completion
if (!m_argsRead) {
return;
}
// print error message and relevant request and response if present
cerr << Phrases::Override << Phrases::Error << message.toLocal8Bit().data() << Phrases::End;
const auto url(request.url());
if (!url.isEmpty()) {
cerr << "\nRequest: " << url.toString(QUrl::PrettyDecoded).toLocal8Bit().data() << '\n';
}
if (!response.isEmpty()) {
cerr << "\nResponse:\n" << response.data() << '\n';
}
cerr << flush;
QCoreApplication::exit(-3);
}
void Application::requestLog(const ArgumentOccurrence &)
{
connect(&m_connection, &SyncthingConnection::logAvailable, printLog);
m_connection.requestLog();
cerr << "Request log from " << m_settings.syncthingUrl.toLocal8Bit().data() << " ...";
cerr.flush();
}
void Application::requestShutdown(const ArgumentOccurrence &)
{
connect(&m_connection, &SyncthingConnection::shutdownTriggered, &QCoreApplication::quit);
m_connection.shutdown();
cerr << "Request shutdown " << m_settings.syncthingUrl.toLocal8Bit().data() << " ...";
cerr.flush();
}
void Application::requestRestart(const ArgumentOccurrence &)
{
connect(&m_connection, &SyncthingConnection::restartTriggered, &QCoreApplication::quit);
m_connection.restart();
cerr << "Request restart " << m_settings.syncthingUrl.toLocal8Bit().data() << " ...";
cerr.flush();
}
void Application::requestRescan(const ArgumentOccurrence &occurrence)
{
if (!m_argsRead) {
initDirCompletion(m_args.rescan, occurrence);
return;
}
m_expectedResponse = 0;
connect(&m_connection, &SyncthingConnection::rescanTriggered, this, &Application::handleResponse);
for (const char *value : occurrence.values) {
const QString dirIdentifier(argToQString(value));
const RelevantDir relevantDir(findDirectory(dirIdentifier));
if (!relevantDir.dirObj) {
continue;
}
relevantDir.notifyAboutRescan();
m_connection.rescan(relevantDir.dirObj->id, relevantDir.subDir);
++m_expectedResponse;
}
if (!m_expectedResponse) {
cerr << Phrases::Error << "No (valid) directories specified." << Phrases::End << flush;
exit(1);
}
cerr << flush;
}
void Application::requestRescanAll(const ArgumentOccurrence &)
{
m_expectedResponse = m_connection.dirInfo().size();
connect(&m_connection, &SyncthingConnection::rescanTriggered, this, &Application::handleResponse);
cerr << "Request rescanning all directories ..." << endl;
m_connection.rescanAllDirs();
}
void Application::requestPauseResume(bool pause)
{
findRelevantDirsAndDevs(OperationType::PauseResume);
m_expectedResponse = 0;
if (pause) {
connect(&m_connection, &SyncthingConnection::devicePauseTriggered, this, &Application::handleResponse);
connect(&m_connection, &SyncthingConnection::directoryPauseTriggered, this, &Application::handleResponse);
} else {
connect(&m_connection, &SyncthingConnection::deviceResumeTriggered, this, &Application::handleResponse);
connect(&m_connection, &SyncthingConnection::directoryResumeTriggered, this, &Application::handleResponse);
}
if (m_relevantDirs.empty() && m_relevantDevs.empty()) {
cerr << Phrases::Error << "No directories or devices specified." << Phrases::End << flush;
exit(1);
}
if (!m_relevantDirs.empty()) {
QStringList dirIds;
dirIds.reserve(trQuandity(m_relevantDirs.size()));
for (const RelevantDir &dir : m_relevantDirs) {
dirIds << dir.dirObj->id;
}
if (pause) {
cerr << "Request pausing directories ";
} else {
cerr << "Request resuming directories ";
}
cerr << dirIds.join(QStringLiteral(", ")).toLocal8Bit().data() << " ...\n";
if (pause ? m_connection.pauseDirectories(dirIds) : m_connection.resumeDirectories(dirIds)) {
++m_expectedResponse;
}
}
if (!m_relevantDevs.empty()) {
QStringList devIds;
devIds.reserve(trQuandity(m_relevantDirs.size()));
for (const SyncthingDev *dev : m_relevantDevs) {
devIds << dev->id;
}
if (pause) {
cerr << "Request pausing devices ";
} else {
cerr << "Request resuming devices ";
}
cerr << devIds.join(QStringLiteral(", ")).toLocal8Bit().data() << " ...\n";
if (pause ? m_connection.pauseDevice(devIds) : m_connection.resumeDevice(devIds)) {
++m_expectedResponse;
}
}
if (!m_expectedResponse) {
cerr << Phrases::Warning << "No directories or devices altered." << Phrases::End << flush;
exit(0);
}
cerr << flush;
}
void Application::findRelevantDirsAndDevs(OperationType operationType)
{
int dummy;
// find relevant dirs
const bool allDirs = m_args.allDirs.isPresent();
if (!allDirs) {
const Argument &dirArg = m_args.dir;
if (dirArg.isPresent()) {
m_relevantDirs.reserve(dirArg.occurrences());
for (size_t i = 0; i != dirArg.occurrences(); ++i) {
const QString dirIdentifier(argToQString(dirArg.values(i).front()));
const RelevantDir relevantDir(findDirectory(dirIdentifier));
if (relevantDir.dirObj) {
m_relevantDirs.emplace_back(move(relevantDir));
}
}
}
}
// find relevant devs
const bool allDevs = m_args.allDevs.isPresent();
if (!allDevs) {
Argument &devArg = m_args.dev;
if (devArg.isPresent()) {
m_relevantDevs.reserve(devArg.occurrences());
for (size_t i = 0; i != devArg.occurrences(); ++i) {
const SyncthingDev *dev = m_connection.findDevInfo(argToQString(devArg.values(i).front()), dummy);
if (!dev) {
dev = m_connection.findDevInfoByName(argToQString(devArg.values(i).front()), dummy);
}
if (!dev) {
cerr << Phrases::Warning << "Specified device \"" << devArg.values(i).front() << "\" does not exist and will be ignored."
<< Phrases::End;
continue;
}
m_relevantDevs.emplace_back(dev);
}
}
}
// when displaying status information and no stats and no dirs/devs have been specified, just print information for all
const bool displayEverything
= operationType == OperationType::Status && !m_args.stats.isPresent() && m_relevantDirs.empty() && m_relevantDevs.empty();
if (allDirs || (!allDevs && displayEverything)) {
m_relevantDirs.reserve(m_connection.dirInfo().size());
for (const SyncthingDir &dir : m_connection.dirInfo()) {
m_relevantDirs.emplace_back(&dir, QString());
}
}
if (allDevs || (!allDirs && displayEverything)) {
m_relevantDevs.reserve(m_connection.devInfo().size());
for (const SyncthingDev &dev : m_connection.devInfo()) {
m_relevantDevs.emplace_back(&dev);
}
}
}
bool Application::findPwd()
{
const QString pwd(QDir::currentPath());
// find directory for working directory
int dummy;
m_pwd.dirObj = m_connection.findDirInfoByPath(pwd, m_pwd.subDir, dummy);
if (m_pwd) {
return true;
}
// handle error
cerr << Phrases::Error << "The current working directory \"" << pwd.toLocal8Bit().data() << "\" is not (part of) a Syncthing directory.";
cerr << Phrases::End << flush;
QCoreApplication::exit(2);
return false;
}
void Application::printDir(const RelevantDir &relevantDir) const
{
const SyncthingDir *const dir = relevantDir.dirObj;
cout << " - " << TextAttribute::Bold << dir->id.toLocal8Bit().data() << '\n' << TextAttribute::Reset;
printProperty("Label", dir->label);
printProperty("Path", dir->path);
printProperty("Status", dir->statusString());
if (!dir->paused) {
printProperty("Global", directoryStatusString(dir->globalStats), nullptr, 6);
printProperty("Local", directoryStatusString(dir->localStats), nullptr, 6);
}
printProperty("Last scan time", dir->lastScanTime);
printProperty("Last file time", dir->lastFileTime);
printProperty("Last file name", dir->lastFileName);
printProperty("Shared with", dir->deviceNames.isEmpty() ? dir->deviceIds : dir->deviceNames);
printProperty("Download progress", dir->downloadLabel);
if (!dir->completionByDevice.empty()) {
printProperty("Remote progress", dir->areRemotesUpToDate() ? "all up-to-date" : "some need bytes");
for (const auto &completionForDev : dir->completionByDevice) {
printProperty(m_connection.deviceNameOrId(completionForDev.first).toLocal8Bit().data(),
argsToString(dataSizeToString(completionForDev.second.globalBytes - completionForDev.second.needed.bytes), ' ', '/', ' ',
dataSizeToString(completionForDev.second.globalBytes), ' ', '(', static_cast<int>(completionForDev.second.percentage), " %)")
.data(),
nullptr, 6);
}
}
printProperty("Type", dir->dirTypeString());
printProperty("Ignore permissions", dir->ignorePermissions);
printProperty("Auto-normalize", dir->autoNormalize);
printProperty("Rescan interval", rescanIntervalString(dir->rescanInterval, dir->fileSystemWatcherEnabled));
if (dir->fileSystemWatcherEnabled) {
printProperty("File watcher delay", dir->fileSystemWatcherDelay, " seconds");
}
printProperty("Min. free disk percentage", dir->minDiskFreePercentage);
printProperty("Error", dir->globalError);
if (!dir->itemErrors.empty()) {
cout << " Failed items\n";
for (const SyncthingItemError &error : dir->itemErrors) {
printProperty(" - Message", error.message);
printProperty(" File", error.path);
}
}
cout << '\n';
}
void Application::printDev(const SyncthingDev *dev) const
{
cout << " - " << TextAttribute::Bold << dev->name.toLocal8Bit().data() << '\n' << TextAttribute::Reset;
printProperty("ID", dev->id);
printProperty("Status", dev->statusString());
printProperty("Addresses", dev->addresses);
printProperty("Compression", dev->compression);
printProperty("Cert name", dev->certName);
printProperty("Connection address", dev->connectionAddress);
printProperty("Connection type", dev->connectionType);
printProperty("Client version", dev->clientVersion);
printProperty("Last seen", dev->lastSeen);
if (dev->totalIncomingTraffic > 0) {
printProperty("Incoming traffic", dataSizeToString(static_cast<std::uint64_t>(dev->totalIncomingTraffic)).data());
}
if (dev->totalOutgoingTraffic > 0) {
printProperty("Outgoing traffic", dataSizeToString(static_cast<std::uint64_t>(dev->totalOutgoingTraffic)).data());
}
cout << '\n';
}
void Application::printStatus(const ArgumentOccurrence &)
{
findRelevantDirsAndDevs(OperationType::Status);
// display stats
if (m_args.stats.isPresent() || (!m_args.dir.isPresent() && !m_args.dev.isPresent())) {
cout << TextAttribute::Bold << "Overall statistics\n" << TextAttribute::Reset;
const auto &overallStats(m_connection.computeOverallDirStatistics());
const auto *statusString = "idle";
const auto *statusColor = "32";
if (m_connection.hasOutOfSyncDirs()) {
statusString = "out-of-sync";
statusColor = "31";
} else {
switch (m_connection.status()) {
case SyncthingStatus::Synchronizing:
statusString = "synchronizing";
statusColor = "34";
break;
case SyncthingStatus::RemoteNotInSync:
statusString = "remote synchronizing";
statusColor = "34";
break;
case SyncthingStatus::Scanning:
statusString = "scanning";
statusColor = "34";
break;
default:;
}
}
if (!EscapeCodes::enabled) {
printProperty("Status", statusString);
} else {
printProperty("Status", argsToString('\e', '[', statusColor, 'm', statusString, "\e[0m"));
}
printProperty("Global", directoryStatusString(overallStats.global), nullptr, 6);
printProperty("Local", directoryStatusString(overallStats.local), nullptr, 6);
printProperty("Incoming traffic", trafficString(m_connection.totalIncomingTraffic(), m_connection.totalIncomingRate()));
printProperty("Outgoing traffic", trafficString(m_connection.totalOutgoingTraffic(), m_connection.totalOutgoingRate()));
const auto &connectedDevices(m_connection.connectedDevices());
if (connectedDevices.empty()) {
printProperty("Connected to", "no other devices");
} else {
printProperty("Connected to", argsToString(connectedDevices.size(), ' ', connectedDevices.size() == 1 ? "device" : "devices", ':'));
printProperty("", displayNames(connectedDevices), nullptr, 6);
}
printProperty("Uptime", m_connection.uptime().toString(TimeSpanOutputFormat::WithMeasures, true));
printProperty("Version", m_connection.syncthingVersion());
cout << '\n';
}
// display dirs
if (!m_relevantDirs.empty()) {
cout << TextAttribute::Bold << "Directories\n" << TextAttribute::Reset;
std::sort(m_relevantDirs.begin(), m_relevantDirs.end(),
[](const RelevantDir &lhs, const RelevantDir &rhs) { return lhs.dirObj->displayName() < rhs.dirObj->displayName(); });
std::for_each(m_relevantDirs.cbegin(), m_relevantDirs.cend(), bind(&Application::printDir, this, std::placeholders::_1));
}
// display devs
if (!m_relevantDevs.empty()) {
cout << TextAttribute::Bold << "Devices\n" << TextAttribute::Reset;
std::sort(m_relevantDevs.begin(), m_relevantDevs.end(), [](const SyncthingDev *lhs, const SyncthingDev *rhs) {
const auto lhsIsOwn = lhs->status == SyncthingDevStatus::OwnDevice, rhsIsOwn = rhs->status == SyncthingDevStatus::OwnDevice;
return lhsIsOwn != rhsIsOwn ? lhsIsOwn : lhs->displayName() < rhs->displayName();
});
std::for_each(m_relevantDevs.cbegin(), m_relevantDevs.cend(), bind(&Application::printDev, this, std::placeholders::_1));
}
cout.flush();
QCoreApplication::exit();
}
void Application::printLog(const std::vector<SyncthingLogEntry> &logEntries)
{
cerr << Phrases::Override;
for (const SyncthingLogEntry &entry : logEntries) {
const auto when = entry.when.toUtf8();
try {
cout << DateTime::fromIsoStringLocal(when.data()).toString(DateTimeOutputFormat::DateAndTime, true);
} catch (const ConversionException &e) {
cout << when.data();
}
cout << ':' << ' ' << entry.message.toLocal8Bit().data() << '\n';
}
cout.flush();
QCoreApplication::exit();
}
void Application::printConfig(const ArgumentOccurrence &)
{
// disable main event loop since this method is invoked directly as argument callback and we're doing all required async operations during the waitForConfig() call already
m_requiresMainEventLoop = false;
if (!waitForConfig()) {
return;
}
cerr << Phrases::Override;
cout << QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented).data() << flush;
}
void Application::editConfig(const ArgumentOccurrence &)
{
// disable main event loop since this method is invoked directly as argument callback and we're doing all required async operations during the waitForConfig() call already
m_requiresMainEventLoop = false;
// wait until config is available
const bool viaJavaScript(m_args.script.isPresent() || m_args.jsLines.isPresent());
if (!(viaJavaScript ? waitForConfigAndStatus() : waitForConfig())) {
return;
}
cerr << Phrases::Override;
const auto newConfig(viaJavaScript ? editConfigViaScript() : editConfigViaEditor());
if (newConfig.isEmpty()) {
// just return here; an error message should have already been printed by editConfigVia*()
return;
}
// handle "dry-run" case
if (m_args.dryRun.isPresent()) {
cout << newConfig.data();
if (!newConfig.endsWith('\n')) {
cout << '\n';
}
cout << flush;
return;
}
// post new config
cerr << Phrases::Info << "Posting new configuration ..." << TextAttribute::Reset << flush;
if (!waitForSignalsOrFail(bind(&SyncthingConnection::postConfigFromByteArray, ref(m_connection), ref(newConfig)), 0,
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfigTriggered))) {
return;
}
cerr << Phrases::Override << Phrases::Info << "Configuration posted successfully" << Phrases::EndFlush;
}
QByteArray Application::editConfigViaEditor() const
{
// read editor command and options
const auto *const editorArgValue(m_args.editor.firstValue());
const auto editorCommand(editorArgValue ? QString::fromLocal8Bit(editorArgValue) : QString());
if (editorCommand.isEmpty()) {
cerr << Phrases::Error << "No editor command specified. It must be either passed via --editor argument or EDITOR environment variable."
<< Phrases::EndFlush;
return QByteArray();
}
QStringList editorOptions;
if (m_args.editor.isPresent()) {
const auto &editorArgValues(m_args.editor.values());
if (!editorArgValues.empty()) {
editorOptions.reserve(trQuandity(editorArgValues.size()));
for (auto i = editorArgValues.cbegin() + 1, end = editorArgValues.cend(); i != end; ++i) {
editorOptions << QString::fromLocal8Bit(*i);
}
}
}
// write config to temporary file
QTemporaryFile tempFile(QStringLiteral("syncthing-config-XXXXXX.json"));
if (!tempFile.open() || !tempFile.write(QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented))) {
cerr << Phrases::Error << "Unable to write the configuration to a temporary file." << Phrases::EndFlush;
return QByteArray();
}
editorOptions << tempFile.fileName();
tempFile.close();
// open editor and wait until it has finished
cerr << Phrases::Info << "Waiting till editor closed ..." << TextAttribute::Reset << flush;
QProcess editor;
editor.setProcessChannelMode(QProcess::ForwardedChannels);
editor.setInputChannelMode(QProcess::ForwardedInputChannel);
editor.start(editorCommand, editorOptions);
editor.waitForFinished(-1);
cerr << Phrases::Override;
// handle editor crash
if (editor.exitStatus() == QProcess::CrashExit) {
cerr << Phrases::Error << "Editor crashed with exit code " << editor.exitCode() << Phrases::End << "invocation command: " << editorArgValue;
if (m_args.editor.isPresent()) {
const auto &editorArgValues(m_args.editor.values());
if (!editorArgValues.empty()) {
for (auto i = editorArgValues.cbegin() + 1, end = editorArgValues.cend(); i != end; ++i) {
cerr << ' ' << *i;
}
}
}
cerr << endl;
return QByteArray();
}
// read (altered) configuration again
QFile tempFile2(editorOptions.back());
if (!tempFile2.open(QIODevice::ReadOnly)) {
cerr << Phrases::Error << "Unable to open temporary file containing the configuration again." << Phrases::EndFlush;
return QByteArray();
}
const auto newConfig(tempFile2.readAll());
if (newConfig.isEmpty()) {
cerr << Phrases::Error << "Unable to read any bytes from temporary file containing the configuration." << Phrases::EndFlush;
return QByteArray();
}
// convert the config to JSON again (could send it to Syncthing as it is, but this allows us to check whether the JSON is valid)
QJsonParseError error;
const auto configDoc(QJsonDocument::fromJson(newConfig, &error));
if (error.error != QJsonParseError::NoError) {
cerr << Phrases::Error << "Unable to parse new configuration" << Phrases::End << "reason: " << error.errorString().toLocal8Bit().data()
<< " at character " << error.offset << endl;
return QByteArray();
}
// perform at least some checks before sending the configuration
const auto configObj(configDoc.object());
if (configObj.isEmpty()) {
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
return QByteArray();
}
if (configObj == m_connection.rawConfig()) {
cerr << Phrases::Warning << "Editing aborted; config hasn't changed." << Phrases::EndFlush;
return QByteArray();
}
for (const auto &arrayName : { QStringLiteral("devices"), QStringLiteral("folders") }) {
if (!configObj.value(arrayName).isArray()) {
cerr << Phrases::Error << "Array \"" << arrayName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
return QByteArray();
}
}
for (const auto &objectName : { QStringLiteral("options"), QStringLiteral("gui") }) {
if (!configObj.value(objectName).isObject()) {
cerr << Phrases::Error << "Object \"" << objectName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
return QByteArray();
}
}
return newConfig;
}
QByteArray Application::editConfigViaScript() const
{
#if defined(SYNCTHINGCTL_USE_SCRIPT) || defined(SYNCTHINGCTL_USE_JSENGINE)
// get script
QByteArray script;
QString scriptFileName;
if (m_args.script.isPresent()) {
// read script file
QFile scriptFile(QString::fromLocal8Bit(m_args.script.firstValue()));
if (!scriptFile.open(QFile::ReadOnly)) {
cerr << Phrases::Error << "Unable to open specified script file \"" << m_args.script.firstValue() << "\"." << Phrases::EndFlush;
return QByteArray();
}
script = scriptFile.readAll();
scriptFileName = scriptFile.fileName();
if (script.isEmpty()) {
cerr << Phrases::Error << "Unable to read any bytes from specified script file \"" << m_args.script.firstValue() << "\"."
<< Phrases::EndFlush;
return QByteArray();
}
} else if (m_args.jsLines.isPresent()) {
// construct script from CLI arguments
auto requiredSize = QString::size_type(0);
for (const auto *const line : m_args.jsLines.values()) {
requiredSize += static_cast<QString::size_type>(std::strlen(line));
requiredSize += 1;
}
script.reserve(requiredSize);
for (const auto *const line : m_args.jsLines.values()) {
script += line;
script += '\n';
}
}
// define function to print error
const auto printError([](const auto &object) {
cerr << object.toString().toLocal8Bit().data() << "\nin line " << SYNCTHINGCTL_JS_INT(object.property(QStringLiteral("lineNumber"))) << endl;
});
// evaluate config via JSON.parse()
SYNCTHINGCTL_JS_ENGINE engine;
auto globalObject(engine.globalObject());
const auto configString(QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented));
globalObject.setProperty(QStringLiteral("configStr"), SYNCTHINGCTL_JS_VALUE(QString::fromUtf8(configString)) SYNCTHINGCTL_JS_READONLY);
const auto configObj(engine.evaluate(QStringLiteral("JSON.parse(configStr)")));
if (configObj.isError()) {
cerr << Phrases::Error << "Unable to evaluate the current Syncthing configuration." << Phrases::End;
printError(configObj);
cerr << "Syncthing configuration: " << configString.data() << flush;
return QByteArray();
}
globalObject.setProperty(QStringLiteral("config"), configObj SYNCTHINGCTL_JS_UNDELETABLE);
// provide additional values
globalObject.setProperty(QStringLiteral("ownID"), m_connection.myId() SYNCTHINGCTL_JS_UNDELETABLE);
globalObject.setProperty(QStringLiteral("url"), m_connection.syncthingUrl() SYNCTHINGCTL_JS_UNDELETABLE);
// provide console.log() which is not available in QJSEngine and QScriptEngine by default (note that print() is only available when using Qt Script)
JSConsole console;
globalObject.setProperty(QStringLiteral("console"), engine.newQObject(&console));
// provide helper
QFile helperFile(QStringLiteral(":/js/helper.js"));
helperFile.open(QFile::ReadOnly);
const auto helperScript(helperFile.readAll());
if (helperScript.isEmpty()) {
cerr << Phrases::Error << "Unable to load internal helper script." << Phrases::EndFlush;
return QByteArray();
}
const auto helperRes(engine.evaluate(QString::fromUtf8(helperScript)));
if (helperRes.isError()) {
cerr << Phrases::Error << "Unable to evaluate internal helper script." << Phrases::End;
printError(helperRes);
return QByteArray();
}
// evaluate the user provided script
const auto res(engine.evaluate(QString::fromUtf8(script), scriptFileName));
if (res.isError()) {
cerr << Phrases::Error << "Unable to evaluate the specified script file \"" << m_args.script.firstValue() << "\"." << Phrases::End;
printError(res);
return QByteArray();
}
// validate the altered configuration
const auto newConfigObj(globalObject.property(QStringLiteral("config")));
if (!newConfigObj.isObject()) {
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
return QByteArray();
}
for (const auto &arrayName : { QStringLiteral("devices"), QStringLiteral("folders") }) {
if (!newConfigObj.property(arrayName).isArray()) {
cerr << Phrases::Error << "Array \"" << arrayName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
return QByteArray();
}
}
for (const auto &objectName : { QStringLiteral("options"), QStringLiteral("gui") }) {
if (!newConfigObj.property(objectName).isObject()) {
cerr << Phrases::Error << "Object \"" << objectName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
return QByteArray();
}
}
// serilaize the altered configuration via JSON.stringify()
const auto newConfigJson(engine.evaluate(QStringLiteral("JSON.stringify(config, null, 4)")));
if (!newConfigJson.isString()) {
cerr << Phrases::Error << "Unable to convert the config object to JSON via JSON.stringify()." << Phrases::End;
cerr << configObj.toString().toLocal8Bit().data() << endl;
return QByteArray();
}
return newConfigJson.toString().toUtf8();
#else
cerr << Phrases::Error << PROJECT_NAME " has not been built with JavaScript support." << Phrases::EndFlush;
return QByteArray();
#endif
}
void Application::waitForIdle(const ArgumentOccurrence &)
{
m_preventDisconnect = true;
// setup timer
QTimer idleTime;
idleTime.setSingleShot(true);
idleTime.setInterval(m_idleDuration);
// define variable which is set to true if handleTimeout to indicate the idle state has persisted long enough
bool isLongEnoughIdle = false;
// define handler for timer timeout
function<void(void)> handleTimeout([this, &isLongEnoughIdle] {
if (checkWhetherIdle()) {
isLongEnoughIdle = true;
}
});
// define handler for dirStatusChanged/devStatusChanged
function<void(void)> handleStatusChange([this, &idleTime] {
if (!checkWhetherIdle()) {
idleTime.stop();
return;
}
if (!idleTime.isActive()) {
idleTime.start();
}
});
// define handler for newDirs/newDevices to call findRelevantDirsAndDevs() in that case
function<void(void)> handleNewDirsOrDevs([this, &handleStatusChange] {
findRelevantDirsAndDevs(OperationType::WaitForIdle);
handleStatusChange();
});
// invoke handler manually because Syncthing could already be idling
handleNewDirsOrDevs();
waitForSignals(&noop, m_idleTimeout, signalInfo(&m_connection, &SyncthingConnection::dirStatusChanged, handleStatusChange, &isLongEnoughIdle),
signalInfo(&m_connection, &SyncthingConnection::devStatusChanged, handleStatusChange, &isLongEnoughIdle),
signalInfo(&m_connection, &SyncthingConnection::newDirs, handleNewDirsOrDevs, &isLongEnoughIdle),
signalInfo(&m_connection, &SyncthingConnection::newDevices, handleNewDirsOrDevs, &isLongEnoughIdle),
signalInfo(&idleTime, &QTimer::timeout, handleTimeout, &isLongEnoughIdle));
if (!isLongEnoughIdle) {
cerr << Phrases::Warning << "Exiting after timeout" << Phrases::End << flush;
}
QCoreApplication::exit(isLongEnoughIdle ? 0 : 1);
}
bool Application::checkWhetherIdle() const
{
for (const RelevantDir &dir : m_relevantDirs) {
switch (dir.dirObj->status) {
case SyncthingDirStatus::Unknown:
case SyncthingDirStatus::Idle:
break;
default:
return false;
}
}
for (const SyncthingDev *dev : m_relevantDevs) {
switch (dev->status) {
case SyncthingDevStatus::Unknown:
case SyncthingDevStatus::Disconnected:
case SyncthingDevStatus::OwnDevice:
case SyncthingDevStatus::Idle:
break;
default:
return false;
}
}
return true;
}
void Application::checkPwdOperationPresent(const ArgumentOccurrence &occurrence)
{
// FIXME: implement default operation in argument parser
if (m_args.pwd.specifiedOperation()) {
return;
}
// print status when no operation specified
printPwdStatus(occurrence);
}
void Application::printPwdStatus(const ArgumentOccurrence &)
{
if (!findPwd()) {
return;
}
printDir(RelevantDir{ m_pwd });
QCoreApplication::quit();
}
void Application::requestRescanPwd(const ArgumentOccurrence &)
{
if (!findPwd()) {
return;
}
m_pwd.notifyAboutRescan();
m_connection.rescan(m_pwd.dirObj->id, m_pwd.subDir);
connect(&m_connection, &SyncthingConnection::rescanTriggered, this, &Application::handleResponse);
m_expectedResponse = 1;
}
void Application::requestPausePwd(const ArgumentOccurrence &)
{
if (!findPwd()) {
return;
}
if (m_connection.pauseDirectories(QStringList(m_pwd.dirObj->id))) {
cerr << "Request pausing directory \"" << m_pwd.dirObj->path.toLocal8Bit().data() << "\" ..." << endl;
connect(&m_connection, &SyncthingConnection::directoryPauseTriggered, this, &Application::handleResponse);
m_preventDisconnect = true;
m_expectedResponse = 1;
} else {
cerr << "Directory \"" << m_pwd.dirObj->path.toLocal8Bit().data() << " already paused" << endl;
QCoreApplication::quit();
}
}
void Application::requestResumePwd(const ArgumentOccurrence &)
{
if (!findPwd()) {
return;
}
if (m_connection.resumeDirectories(QStringList(m_pwd.dirObj->id))) {
cerr << "Request resuming directory \"" << m_pwd.dirObj->path.toLocal8Bit().data() << "\" ..." << endl;
connect(&m_connection, &SyncthingConnection::directoryResumeTriggered, this, &Application::handleResponse);
m_preventDisconnect = true;
m_expectedResponse = 1;
return;
} else {
cerr << "Directory \"" << m_pwd.dirObj->path.toLocal8Bit().data() << " not paused" << endl;
QCoreApplication::quit();
}
}
void Application::initDirCompletion(Argument &arg, const ArgumentOccurrence &)
{
// prevent this initialization if we're not in shell completion mode
if (m_argsRead) {
return;
}
// load config and wait for connected
loadConfig();
waitForConfig();
// set directory IDs as completion values
m_dirCompletion = m_connection.directoryIds().join(QChar(' ')).toUtf8();
arg.setPreDefinedCompletionValues(m_dirCompletion.data());
}
void Application::initDevCompletion(Argument &arg, const ArgumentOccurrence &)
{
// prevent this initialization if we're not in shell completion mode
if (m_argsRead) {
return;
}
// load config and wait for connected
loadConfig();
waitForConfig();
// set device IDs and names as completion values
QStringList completionValues;
const size_t valueCount = m_connection.devInfo().size() << 2;
if (valueCount > numeric_limits<int>::max()) {
return;
}
completionValues.reserve(static_cast<int>(valueCount));
for (const SyncthingDev &dev : m_connection.devInfo()) {
completionValues << dev.id << dev.name;
}
m_devCompletion = completionValues.join(QChar(' ')).toUtf8();
arg.setPreDefinedCompletionValues(m_devCompletion.data());
}
RelevantDir Application::findDirectory(const QString &dirIdentifier)
{
int dummy;
RelevantDir relevantDir;
// check whether the specified identifier is a known Syncthing directory or a relative path to an item in a
// known Syncthing directory
int firstSlash = dirIdentifier.indexOf(QChar('/'));
relevantDir.dirObj = m_connection.findDirInfo(firstSlash >= 0 ? dirIdentifier.mid(0, firstSlash) : dirIdentifier, dummy);
if (relevantDir) {
if (firstSlash >= 0) {
relevantDir.subDir = dirIdentifier.mid(firstSlash + 1);
}
return relevantDir;
}
// check whether the specified identifier is an absolute or relative path of an item inside a known Syncthing directory
relevantDir.dirObj = m_connection.findDirInfoByPath(
QDir::isRelativePath(dirIdentifier) ? QDir::currentPath() % QChar('/') % dirIdentifier : dirIdentifier, relevantDir.subDir, dummy);
if (relevantDir) {
return relevantDir;
}
cerr << Phrases::Warning << "Specified directory \"" << dirIdentifier.toLocal8Bit().data() << "\" is no Syncthing directory (or not part of any)."
<< Phrases::End;
return relevantDir;
}
void RelevantDir::notifyAboutRescan() const
{
cerr << Phrases::Info;
if (subDir.isEmpty()) {
cerr << "Request rescanning directory \"" << dirObj->path.toLocal8Bit().data() << "\" ...";
} else {
cerr << "Request rescanning item \"" << subDir.toLocal8Bit().data() << "\" in directory \"" << dirObj->path.toLocal8Bit().data() << "\" ...";
}
cerr << Phrases::EndFlush;
}
} // namespace Cli