Allow editing config via syncthingctl

This commit is contained in:
Martchus 2018-04-01 22:34:59 +02:00
parent ac36e6955d
commit c7ea5974e4
6 changed files with 157 additions and 9 deletions

View File

@ -19,7 +19,9 @@
#include <QDir>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QStringBuilder>
#include <QTemporaryFile>
#include <QTimer>
#include <functional>
@ -79,6 +81,7 @@ Application::Application()
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));
@ -576,11 +579,114 @@ void Application::printLog(const std::vector<SyncthingLogEntry> &logEntries)
void Application::printConfig(const ArgumentOccurrence &)
{
waitForConfig();
eraseLine(cout);
cout << '\r' << QJsonDocument(m_connection.rawConfig()).toJson().data() << flush;
// disable main event loop since this method is invoked directly as argument callback and we've done all async operations during the waitForConfig() call already
// 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;
// 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;
}
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);
}
}
}
// wait until config is available
if (!waitForConfig()) {
return;
}
cerr << Phrases::Override;
// 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;
}
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;
}
// 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;
}
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;
}
// 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;
}
const auto configObj(configDoc.object());
if (configObj.isEmpty()) {
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
return;
}
// post new config
using namespace TestUtilities;
cerr << Phrases::Info << "Posting new configuration ..." << TextAttribute::Reset << flush;
if (!waitForSignalsOrFail(bind(&SyncthingConnection::postConfig, ref(m_connection), ref(configObj)), 0,
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfigTriggered))) {
return;
}
cerr << Phrases::Override << Phrases::Info << "Configuration posted successfully" << Phrases::EndFlush;
}
void Application::waitForIdle(const ArgumentOccurrence &)

View File

@ -66,6 +66,7 @@ private:
void printStatus(const ArgumentOccurrence &);
static void printLog(const std::vector<Data::SyncthingLogEntry> &logEntries);
void printConfig(const ArgumentOccurrence &);
void editConfig(const ArgumentOccurrence &);
void waitForIdle(const ArgumentOccurrence &);
bool checkWhetherIdle() const;
void checkPwdOperationPresent(const ArgumentOccurrence &occurrence);

View File

@ -16,7 +16,8 @@ Args::Args()
, resume("resume", '\0', "resumes the specified directories and devices")
, waitForIdle("wait-for-idle", 'w', "waits until the specified dirs/devs are idling")
, pwd("pwd", 'p', "operates in the current working directory")
, cat("cat", '\0', "prints the current configuration")
, cat("cat", '\0', "prints the current Syncthing configuration")
, edit("edit", '\0', "allows editing the Syncthing configuration using an external editor")
, statusPwd("status", 's', "prints the status of the current working directory")
, rescanPwd("rescan", 'r', "rescans the current working directory")
, pausePwd("pause", 'p', "pauses the current working directory")
@ -28,6 +29,7 @@ Args::Args()
, atLeast("at-least", 'a', "specifies for how many milliseconds Syncthing must idle (prevents exiting too early in case of flaky status)",
{ "number" })
, timeout("timeout", 't', "specifies how many milliseconds to wait at most", { "number" })
, editor("editor", '\0', "specifies the editor to be opened", { "editor name", "editor option" })
, configFile("config-file", 'f', "specifies the Syncthing config file to read API key and URL from, when not explicitely specified", { "path" })
, apiKey("api-key", 'k', "specifies the API key", { "key" })
, url("url", 'u', "specifies the Syncthing URL, default is http://localhost:8080", { "URL" })
@ -45,6 +47,7 @@ Args::Args()
waitForIdle.setExample(PROJECT_NAME " wait-for-idle --timeout 1800000 --at-least 5000 && systemctl poweroff\n" PROJECT_NAME
" wait-for-idle --dir dir1 --dir dir2 --dev dev1 --dev dev2 --at-least 5000");
pwd.setSubArguments({ &statusPwd, &rescanPwd, &pausePwd, &resumePwd });
edit.setSubArguments({ &editor });
rescan.setValueNames({ "dir ID" });
rescan.setRequiredValueCount(Argument::varValueCount);
@ -56,11 +59,14 @@ Args::Args()
resume.setSubArguments({ &dir, &dev, &allDirs, &allDevs });
resume.setExample(PROJECT_NAME " resume --dir dir1 --dir dir2 --dev dev1 --dev dev2\n" PROJECT_NAME " resume --all-devs");
editor.setEnvironmentVariable("EDITOR");
editor.setRequiredValueCount(Argument::varValueCount);
configFile.setExample(PROJECT_NAME " status --dir dir1 --config-file ~/.config/syncthing/config.xml");
credentials.setExample(PROJECT_NAME " status --dir dir1 --credentials name supersecret");
parser.setMainArguments({ &status, &log, &stop, &restart, &rescan, &rescanAll, &pause, &resume, &waitForIdle, &pwd, &cat, &configFile, &apiKey,
&url, &credentials, &certificate, &noColor, &help });
parser.setMainArguments({ &status, &log, &stop, &restart, &rescan, &rescanAll, &pause, &resume, &waitForIdle, &pwd, &cat, &edit, &configFile,
&apiKey, &url, &credentials, &certificate, &noColor, &help });
// allow setting default values via environment
configFile.setEnvironmentVariable("SYNCTHING_CTL_CONFIG_FILE");

View File

@ -12,10 +12,11 @@ struct Args {
ArgumentParser parser;
HelpArgument help;
NoColorArgument noColor;
OperationArgument status, log, stop, restart, rescan, rescanAll, pause, resume, waitForIdle, pwd, cat;
OperationArgument status, log, stop, restart, rescan, rescanAll, pause, resume, waitForIdle, pwd, cat, edit;
OperationArgument statusPwd, rescanPwd, pausePwd, resumePwd;
ConfigValueArgument dir, dev, allDirs, allDevs;
ConfigValueArgument atLeast, timeout;
ConfigValueArgument editor;
ConfigValueArgument configFile, apiKey, url, credentials, certificate;
};

View File

@ -827,6 +827,17 @@ void SyncthingConnection::requestDeviceStatistics()
requestData(QStringLiteral("stats/device"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDeviceStatistics);
}
/*!
* \brief Posts the specified \a rawConfig.
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
*/
void SyncthingConnection::postConfig(const QJsonObject &rawConfig)
{
QObject::connect(postData(QStringLiteral("system/config"), QUrlQuery(), QJsonDocument(rawConfig).toJson(QJsonDocument::Compact)),
&QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
}
/*!
* \brief Requests the Syncthing events (since the last successful call) asynchronously.
*
@ -1897,6 +1908,19 @@ void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJson
}
}
void SyncthingConnection::readPostConfig()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit newConfigTriggered();
break;
default:
emitError(tr("Unable to post config: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Reads results of rescan().
*/
@ -2340,6 +2364,12 @@ void SyncthingConnection::recalculateStatus()
* \brief Indicates totalIncomingTraffic() or totalOutgoingTraffic() has changed.
*/
/*!
* \fn SyncthingConnection::newConfigTriggered()
* \brief Indicates a new configuration has posted sucessfully via postConfig().
* \remarks In contrast to newConfig(), this signal is only emitted for configuration changes internally posted via postConfig().
*/
/*!
* \fn SyncthingConnection::rescanTriggered()
* \brief Indicates a rescan has been triggered sucessfully.

View File

@ -79,6 +79,7 @@ class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingConnection : public QObject {
Q_PROPERTY(double totalIncomingRate READ totalIncomingRate NOTIFY trafficChanged)
Q_PROPERTY(double totalOutgoingRate READ totalOutgoingRate NOTIFY trafficChanged)
Q_PROPERTY(std::size_t connectedDevices READ connectedDevices)
Q_PROPERTY(QJsonObject rawConfig READ rawConfig NOTIFY newConfig)
public:
explicit SyncthingConnection(
@ -162,9 +163,10 @@ public Q_SLOTS:
void requestDirStatus(const QString &dirId);
void requestCompletion(const QString &devId, const QString &dirId);
void requestDeviceStatistics();
void postConfig(const QJsonObject &rawConfig);
Q_SIGNALS:
void newConfig(const QJsonObject &config);
void newConfig(const QJsonObject &rawConfig);
void newDirs(const std::vector<SyncthingDir> &dirs);
void newDevices(const std::vector<SyncthingDev> &devs);
void newEvents(const QJsonArray &events);
@ -179,6 +181,7 @@ Q_SIGNALS:
void configDirChanged(const QString &newConfigDir);
void myIdChanged(const QString &myNewId);
void trafficChanged(uint64 totalIncomingTraffic, uint64 totalOutgoingTraffic);
void newConfigTriggered();
void rescanTriggered(const QString &dirId);
void devicePauseTriggered(const QStringList &devIds);
void deviceResumeTriggered(const QStringList &devIds);
@ -217,6 +220,7 @@ private Q_SLOTS:
void readRemoteFolderCompletion(
ChronoUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId);
void readRemoteIndexUpdated(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData);
void readPostConfig();
void readRescan();
void readDevPauseResume();
void readDirPauseResume();