syncevolution/src/syncevo/LocalTransportAgent.cpp

1212 lines
47 KiB
C++

/*
* Copyright (C) 2010 Patrick Ohly
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) version 3.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#include <syncevo/LocalTransportAgent.h>
#include <syncevo/SyncContext.h>
#include <syncevo/SyncML.h>
#include <syncevo/LogRedirect.h>
#include <syncevo/StringDataBlob.h>
#include <syncevo/IniConfigNode.h>
#include <syncevo/GLibSupport.h>
#include <syncevo/DBusTraits.h>
#include <syncevo/SuspendFlags.h>
#include <syncevo/LogRedirect.h>
#include <syncevo/LogDLT.h>
#include <syncevo/BoostHelper.h>
#include <synthesis/syerror.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pcrecpp.h>
#include <algorithm>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
class NoopAgentDestructor
{
public:
void operator () (TransportAgent *agent) throw() {}
};
LocalTransportAgent::LocalTransportAgent(SyncContext *server,
const std::string &clientConfig,
void *loop) :
m_server(server),
m_clientConfig(SyncConfig::normalizeConfigString(clientConfig)),
m_status(INACTIVE),
m_loop(loop ?
GMainLoopCXX(static_cast<GMainLoop *>(loop), ADD_REF) :
GMainLoopCXX(g_main_loop_new(NULL, false), TRANSFER_REF))
{
}
boost::shared_ptr<LocalTransportAgent> LocalTransportAgent::create(SyncContext *server,
const std::string &clientConfig,
void *loop)
{
boost::shared_ptr<LocalTransportAgent> self(new LocalTransportAgent(server, clientConfig, loop));
self->m_self = self;
return self;
}
LocalTransportAgent::~LocalTransportAgent()
{
}
void LocalTransportAgent::start()
{
// TODO (?): check that there are no conflicts between the active
// sources. The old "contexts must be different" check achieved that
// via brute force (because by definition, databases from different
// contexts are meant to be independent), but it was too coarse
// and ruled out valid configurations.
// if (m_clientContext == m_server->getContextName()) {
// SE_THROW(StringPrintf("invalid local sync inside context '%s', need second context with different databases", context.c_str()));
// }
if (m_forkexec) {
SE_THROW("local transport already started");
}
m_status = ACTIVE;
m_forkexec = ForkExecParent::create("syncevo-local-sync");
#ifdef USE_DLT
if (getenv("SYNCEVOLUTION_USE_DLT")) {
m_forkexec->addEnvVar("SYNCEVOLUTION_USE_DLT", StringPrintf("%d", LoggerDLT::getCurrentDLTLogLevel()));
}
#endif
m_forkexec->m_onConnect.connect(boost::bind(&LocalTransportAgent::onChildConnect, this, _1));
// fatal problems, including quitting child with non-zero status
m_forkexec->m_onFailure.connect(boost::bind(&LocalTransportAgent::onFailure, this, _2));
// watch onQuit and remember whether the child is still running,
// because it might quit prematurely with a zero return code (for
// example, when an unexpected slow sync is detected)
m_forkexec->m_onQuit.connect(boost::bind(&LocalTransportAgent::onChildQuit, this, _1));
m_forkexec->start();
}
/**
* Uses the D-Bus API provided by LocalTransportParent.
*/
class LocalTransportParent : private GDBusCXX::DBusRemoteObject
{
public:
static const char *path() { return "/"; }
static const char *interface() { return "org.syncevolution.localtransport.parent"; }
static const char *destination() { return "local.destination"; }
static const char *askPasswordName() { return "AskPassword"; }
static const char *storeSyncReportName() { return "StoreSyncReport"; }
LocalTransportParent(const GDBusCXX::DBusConnectionPtr &conn) :
GDBusCXX::DBusRemoteObject(conn, path(), interface(), destination()),
m_askPassword(*this, askPasswordName()),
m_storeSyncReport(*this, storeSyncReportName())
{}
/** LocalTransportAgent::askPassword() */
GDBusCXX::DBusClientCall1<std::string> m_askPassword;
/** LocalTransportAgent::storeSyncReport() */
GDBusCXX::DBusClientCall0 m_storeSyncReport;
};
/**
* Uses the D-Bus API provided by LocalTransportAgentChild.
*/
class LocalTransportChild : public GDBusCXX::DBusRemoteObject
{
public:
static const char *path() { return "/"; }
static const char *interface() { return "org.syncevolution.localtransport.child"; }
static const char *destination() { return "local.destination"; }
static const char *logOutputName() { return "LogOutput"; }
static const char *setFreezeName() { return "SetFreeze"; }
static const char *startSyncName() { return "StartSync"; }
static const char *sendMsgName() { return "SendMsg"; }
LocalTransportChild(const GDBusCXX::DBusConnectionPtr &conn) :
GDBusCXX::DBusRemoteObject(conn, path(), interface(), destination()),
m_logOutput(*this, logOutputName(), false),
m_setFreeze(*this, setFreezeName()),
m_startSync(*this, startSyncName()),
m_sendMsg(*this, sendMsgName())
{}
/**
* information from server config about active sources:
* mapping is from server source names to child source name + sync mode
* (again as set on the server side!)
*/
typedef std::map<std::string, StringPair> ActiveSources_t;
/** use this to send a message back from child to parent */
typedef boost::shared_ptr< GDBusCXX::Result2< std::string, GDBusCXX::DBusArray<uint8_t> > > ReplyPtr;
/** log output with level and message; process name will be added by parent */
GDBusCXX::SignalWatch2<string, string> m_logOutput;
/** LocalTransportAgentChild::setFreeze() */
GDBusCXX::DBusClientCall0 m_setFreeze;
/** LocalTransportAgentChild::startSync() */
GDBusCXX::DBusClientCall2<std::string, GDBusCXX::DBusArray<uint8_t> > m_startSync;
/** LocalTransportAgentChild::sendMsg() */
GDBusCXX::DBusClientCall2<std::string, GDBusCXX::DBusArray<uint8_t> > m_sendMsg;
};
void LocalTransportAgent::logChildOutput(const std::string &level, const std::string &message)
{
Logger::MessageOptions options(Logger::strToLevel(level.c_str()));
options.m_processName = &m_clientConfig;
// Child should have written this into its own log file and/or syslog/dlt already.
// Only pass it on to a user of the command line interface.
options.m_flags = Logger::MessageOptions::ALREADY_LOGGED;
SyncEvo::Logger::instance().messageWithOptions(options, "%s", message.c_str());
}
void LocalTransportAgent::onChildConnect(const GDBusCXX::DBusConnectionPtr &conn)
{
SE_LOG_DEBUG(NULL, "child is ready");
m_parent.reset(new GDBusCXX::DBusObjectHelper(conn,
LocalTransportParent::path(),
LocalTransportParent::interface(),
GDBusCXX::DBusObjectHelper::Callback_t(),
true));
m_parent->add(this, &LocalTransportAgent::askPassword, LocalTransportParent::askPasswordName());
m_parent->add(this, &LocalTransportAgent::storeSyncReport, LocalTransportParent::storeSyncReportName());
m_parent->activate();
m_child.reset(new LocalTransportChild(conn));
m_child->m_logOutput.activate(boost::bind(&LocalTransportAgent::logChildOutput, this, _1, _2));
// now tell child what to do
LocalTransportChild::ActiveSources_t sources;
BOOST_FOREACH(const string &sourceName, m_server->getSyncSources()) {
SyncSourceNodes nodes = m_server->getSyncSourceNodesNoTracking(sourceName);
SyncSourceConfig source(sourceName, nodes);
std::string sync = source.getSync();
if (sync != "disabled") {
string targetName = source.getURINonEmpty();
sources[sourceName] = std::make_pair(targetName, sync);
}
}
m_child->m_startSync.start(m_clientConfig,
StringPair(m_server->getConfigName(),
m_server->isEphemeral() ?
"ephemeral" :
m_server->getRootPath()),
static_cast<std::string>(m_server->getLogDir()),
m_server->getDoLogging(),
std::make_pair(m_server->getSyncUser(),
m_server->getSyncPassword()),
m_server->getConfigProps(),
sources,
boost::bind(&LocalTransportAgent::storeReplyMsg, m_self, _1, _2, _3));
}
void LocalTransportAgent::onFailure(const std::string &error)
{
m_status = FAILED;
g_main_loop_quit(m_loop.get());
SE_LOG_ERROR(NULL, "local transport failed: %s", error.c_str());
m_parent.reset();
m_child.reset();
}
void LocalTransportAgent::onChildQuit(int status)
{
SE_LOG_DEBUG(NULL, "child process has quit with status %d", status);
g_main_loop_quit(m_loop.get());
}
static void GotPassword(const boost::shared_ptr< GDBusCXX::Result1<const std::string &> > &reply,
const std::string &password)
{
reply->done(password);
}
static void PasswordException(const boost::shared_ptr< GDBusCXX::Result1<const std::string &> > &reply)
{
// TODO: refactor, this is the same as dbusErrorCallback
try {
// If there is no pending exception, the process will abort
// with "terminate called without an active exception";
// dbusErrorCallback() should only be called when there is
// a pending exception.
// TODO: catch this misuse in a better way
throw;
} catch (...) {
// let D-Bus parent log the error
std::string explanation;
Exception::handle(explanation, HANDLE_EXCEPTION_NO_ERROR);
reply->failed(GDBusCXX::dbus_error("org.syncevolution.localtransport.error", explanation));
}
}
void LocalTransportAgent::askPassword(const std::string &passwordName,
const std::string &descr,
const ConfigPasswordKey &key,
const boost::shared_ptr< GDBusCXX::Result1<const std::string &> > &reply)
{
// pass that work to our own SyncContext and its UI - currently blocks
SE_LOG_DEBUG(NULL, "local sync parent: asked for password %s, %s",
passwordName.c_str(),
descr.c_str());
try {
if (m_server) {
m_server->getUserInterfaceNonNull().askPasswordAsync(passwordName, descr, key,
// TODO refactor: use dbus-callbacks.h
boost::bind(GotPassword,
reply,
_1),
boost::bind(PasswordException,
reply));
} else {
SE_LOG_DEBUG(NULL, "local sync parent: password request failed because no m_server");
reply->failed(GDBusCXX::dbus_error("org.syncevolution.localtransport.error",
"not connected to UI"));
}
} catch (...) {
PasswordException(reply);
}
}
void LocalTransportAgent::storeSyncReport(const std::string &report)
{
SE_LOG_DEBUG(NULL, "got child sync report:\n%s",
report.c_str());
m_clientReport = SyncReport(report);
}
void LocalTransportAgent::getClientSyncReport(SyncReport &report)
{
report = m_clientReport;
}
void LocalTransportAgent::setContentType(const std::string &type)
{
m_contentType = type;
}
// workaround for limitations of bind+signals when used together with plain GMainLoop pointer
// (pointer to undefined struct)
static void gMainLoopQuit(GMainLoopCXX *loop)
{
g_main_loop_quit(loop->get());
}
void LocalTransportAgent::shutdown()
{
SE_LOG_DEBUG(NULL, "parent is shutting down");
if (m_forkexec) {
// block until child is done
boost::signals2::scoped_connection c(m_forkexec->m_onQuit.connect(boost::bind(gMainLoopQuit,
&m_loop)));
// don't kill the child here - we expect it to complete by
// itself at some point
// TODO: how do we detect a child which gets stuck after its last
// communication with the parent?
// m_forkexec->stop();
while (m_forkexec->getState() != ForkExecParent::TERMINATED) {
SE_LOG_DEBUG(NULL, "waiting for child to stop");
g_main_loop_run(m_loop.get());
}
m_forkexec.reset();
m_parent.reset();
m_child.reset();
}
}
void LocalTransportAgent::setFreeze(bool freeze)
{
// Relay to other side, check for error exception synchronously.
if (m_child) {
m_child->m_setFreeze(freeze);
}
}
void LocalTransportAgent::send(const char *data, size_t len)
{
if (m_child) {
m_status = ACTIVE;
m_child->m_sendMsg.start(m_contentType, GDBusCXX::makeDBusArray(len, (uint8_t *)(data)),
boost::bind(&LocalTransportAgent::storeReplyMsg, m_self, _1, _2, _3));
} else {
m_status = FAILED;
SE_THROW_EXCEPTION(TransportException,
"cannot send message because child process is gone");
}
}
void LocalTransportAgent::storeReplyMsg(const std::string &contentType,
const GDBusCXX::DBusArray<uint8_t> &reply,
const std::string &error)
{
m_replyMsg.assign(reinterpret_cast<const char *>(reply.second),
reply.first);
m_replyContentType = contentType;
if (error.empty()) {
m_status = GOT_REPLY;
} else {
// Only an error if the client hasn't shut down normally.
if (m_clientReport.empty()) {
SE_LOG_ERROR(NULL, "sending message to child failed: %s", error.c_str());
m_status = FAILED;
}
}
g_main_loop_quit(m_loop.get());
}
void LocalTransportAgent::cancel()
{
if (m_forkexec) {
SE_LOG_DEBUG(NULL, "killing local transport child in cancel()");
m_forkexec->stop();
}
m_status = CANCELED;
}
TransportAgent::Status LocalTransportAgent::wait(bool noReply)
{
if (m_status == ACTIVE) {
// need next message; for noReply == true we are done
if (noReply) {
m_status = INACTIVE;
} else {
while (m_status == ACTIVE) {
SE_LOG_DEBUG(NULL, "waiting for child to send message");
if (m_forkexec &&
m_forkexec->getState() == ForkExecParent::TERMINATED) {
m_status = FAILED;
if (m_clientReport.getStatus() != STATUS_OK &&
m_clientReport.getStatus() != STATUS_HTTP_OK) {
// Report that status, with an error message which contains the explanation
// added to the client's error. We are a bit fuzzy about matching the status:
// 10xxx matches xxx and vice versa.
int status = m_clientReport.getStatus();
if (status >= sysync::LOCAL_STATUS_CODE && status <= sysync::LOCAL_STATUS_CODE_END) {
status -= sysync::LOCAL_STATUS_CODE;
}
std::string explanation = StringPrintf("failure on target side %s of local sync",
m_clientConfig.c_str());
static const pcrecpp::RE re("\\((?:local|remote), status (\\d+)\\): (.*)");
int clientStatus;
std::string clientExplanation;
if (re.PartialMatch(m_clientReport.getError(), &clientStatus, &clientExplanation) &&
(status == clientStatus ||
status == clientStatus - sysync::LOCAL_STATUS_CODE)) {
explanation += ": ";
explanation += clientExplanation;
}
SE_THROW_EXCEPTION_STATUS(StatusException,
explanation,
m_clientReport.getStatus());
} else {
SE_THROW_EXCEPTION(TransportException,
"child process quit without sending its message");
}
}
g_main_loop_run(m_loop.get());
}
}
}
return m_status;
}
void LocalTransportAgent::getReply(const char *&data, size_t &len, std::string &contentType)
{
if (m_status != GOT_REPLY) {
SE_THROW("internal error, no reply available");
}
contentType = m_replyContentType;
data = m_replyMsg.c_str();
len = m_replyMsg.size();
}
void LocalTransportAgent::setTimeout(int seconds)
{
// setTimeout() was meant for unreliable transports like HTTP
// which cannot determine whether the peer is still alive. The
// LocalTransportAgent uses sockets and will notice when a peer
// dies unexpectedly, so timeouts should never be necessary.
//
// Quite the opposite, because the "client" in a local sync
// with WebDAV on the client side can be quite slow, incorrect
// timeouts were seen where the client side took longer than
// the default timeout of 5 minutes to process a message and
// send a reply.
//
// Therefore we ignore the request to set a timeout here and thus
// local send/receive operations are allowed to continue for as
// long as they like.
//
// m_timeoutSeconds = seconds;
}
class LocalTransportUI : public UserInterface
{
boost::shared_ptr<LocalTransportParent> m_parent;
public:
LocalTransportUI(const boost::shared_ptr<LocalTransportParent> &parent) :
m_parent(parent)
{}
/** implements password request by asking the parent via D-Bus */
virtual string askPassword(const string &passwordName,
const string &descr,
const ConfigPasswordKey &key)
{
SE_LOG_DEBUG(NULL, "local transport child: requesting password %s, %s via D-Bus",
passwordName.c_str(),
descr.c_str());
std::string password;
std::string error;
bool havePassword = false;
m_parent->m_askPassword.start(passwordName, descr, key,
boost::bind(&LocalTransportUI::storePassword, this,
boost::ref(password), boost::ref(error),
boost::ref(havePassword),
_1, _2));
SuspendFlags &s = SuspendFlags::getSuspendFlags();
while (!havePassword) {
if (s.getState() != SuspendFlags::NORMAL) {
SE_THROW_EXCEPTION_STATUS(StatusException,
StringPrintf("User did not provide the '%s' password.",
passwordName.c_str()),
SyncMLStatus(sysync::LOCERR_USERABORT));
}
g_main_context_iteration(NULL, true);
}
if (!error.empty()) {
Exception::tryRethrowDBus(error);
SE_THROW(StringPrintf("retrieving password failed: %s", error.c_str()));
}
return password;
}
virtual bool savePassword(const std::string &passwordName, const std::string &password, const ConfigPasswordKey &key) { SE_THROW("not implemented"); return false; }
virtual void readStdin(std::string &content) { SE_THROW("not implemented"); }
private:
void storePassword(std::string &res, std::string &errorRes, bool &haveRes, const std::string &password, const std::string &error)
{
if (!error.empty()) {
SE_LOG_DEBUG(NULL, "local transport child: D-Bus password request failed: %s",
error.c_str());
errorRes = error;
} else {
SE_LOG_DEBUG(NULL, "local transport child: D-Bus password request succeeded");
res = password;
}
haveRes = true;
}
};
static void abortLocalSync(int sigterm)
{
// logging anything here is not safe (our own logging system might
// have been interrupted by the SIGTERM and thus be in an inconsistent
// state), but let's try it anyway
SE_LOG_INFO(NULL, "local sync child shutting down due to SIGTERM");
// raise the signal again after disabling the handler, to ensure that
// the exit status is "killed by signal xxx" - good because then
// the whoever killed used gets the information that we didn't die for
// some other reason
signal(sigterm, SIG_DFL);
raise(sigterm);
}
/**
* Provides the "LogOutput" signal.
* LocalTransportAgentChild adds the method implementations
* before activating it.
*/
class LocalTransportChildImpl : public GDBusCXX::DBusObjectHelper
{
public:
LocalTransportChildImpl(const GDBusCXX::DBusConnectionPtr &conn) :
GDBusCXX::DBusObjectHelper(conn,
LocalTransportChild::path(),
LocalTransportChild::interface(),
GDBusCXX::DBusObjectHelper::Callback_t(),
true),
m_logOutput(*this, LocalTransportChild::logOutputName())
{
add(m_logOutput);
};
GDBusCXX::EmitSignal2<std::string,
std::string,
true /* ignore transmission failures */> m_logOutput;
};
class ChildLogger : public Logger
{
std::auto_ptr<LogRedirect> m_parentLogger;
boost::weak_ptr<LocalTransportChildImpl> m_child;
public:
ChildLogger(const boost::shared_ptr<LocalTransportChildImpl> &child) :
m_parentLogger(new LogRedirect(LogRedirect::STDERR_AND_STDOUT)),
m_child(child)
{}
~ChildLogger()
{
m_parentLogger.reset();
}
/**
* Write message into our own log and send to parent.
*/
virtual void messagev(const MessageOptions &options,
const char *format,
va_list args)
{
if (options.m_level <= m_parentLogger->getLevel()) {
m_parentLogger->process();
boost::shared_ptr<LocalTransportChildImpl> child = m_child.lock();
if (child) {
// prefix is used to set session path
// for general server output, the object path field is dbus server
// the object path can't be empty for object paths prevent using empty string.
string strLevel = Logger::levelToStr(options.m_level);
string log = StringPrintfV(format, args);
child->m_logOutput(strLevel, log);
}
}
}
};
class LocalTransportAgentChild : public TransportAgent
{
/** final return code of our main(): non-zero indicates that we need to shut down */
int m_ret;
/**
* sync report for client side of the local sync
*/
SyncReport m_clientReport;
/** used to capture libneon output */
boost::scoped_ptr<LogRedirect> m_parentLogger;
/**
* provides connection to parent, created in constructor
*/
boost::shared_ptr<ForkExecChild> m_forkexec;
/**
* proxy for the parent's D-Bus API in onConnect()
*/
boost::shared_ptr<LocalTransportParent> m_parent;
/**
* our D-Bus interface, created in onConnect()
*/
boost::shared_ptr<LocalTransportChildImpl> m_child;
/**
* sync context, created in Sync() D-Bus call
*/
boost::scoped_ptr<SyncContext> m_client;
/**
* use this D-Bus result handle to send a message from child to parent
* in response to sync() or (later) sendMsg()
*/
LocalTransportChild::ReplyPtr m_msgToParent;
void setMsgToParent(const LocalTransportChild::ReplyPtr &reply,
const std::string &reason)
{
if (m_msgToParent) {
m_msgToParent->failed(GDBusCXX::dbus_error("org.syncevolution.localtransport.error",
"cancelling message: " + reason));
}
m_msgToParent = reply;
}
/** content type for message to parent */
std::string m_contentType;
/**
* message from parent
*/
std::string m_message;
/**
* content type of message from parent
*/
std::string m_messageType;
/**
* true after parent has received sync report, or sending failed
*/
bool m_reportSent;
/**
* INACTIVE when idle,
* ACTIVE after having sent and while waiting for next message,
* GOT_REPLY when we have a message to be processed,
* FAILED when permanently broken
*/
Status m_status;
/**
* one loop run + error checking
*/
void step(const std::string &status)
{
SE_LOG_DEBUG(NULL, "local transport: %s", status.c_str());
if (!m_forkexec ||
m_forkexec->getState() == ForkExecChild::DISCONNECTED) {
SE_THROW("local transport child no longer has a parent, terminating");
}
g_main_context_iteration(NULL, true);
if (m_ret) {
SE_THROW("local transport child encountered a problem, terminating");
}
}
static void onParentQuit()
{
// Never free this state blocker. We can only abort and
// quit from now on.
static boost::shared_ptr<SuspendFlags::StateBlocker> abortGuard;
SE_LOG_ERROR(NULL, "sync parent quit unexpectedly");
abortGuard = SuspendFlags::getSuspendFlags().abort();
}
void onConnect(const GDBusCXX::DBusConnectionPtr &conn)
{
SE_LOG_DEBUG(NULL, "child connected to parent");
// provide our own API
m_child.reset(new LocalTransportChildImpl(conn));
m_child->add(this, &LocalTransportAgentChild::setFreezeLocalSync, LocalTransportChild::setFreezeName());
m_child->add(this, &LocalTransportAgentChild::startSync, LocalTransportChild::startSyncName());
m_child->add(this, &LocalTransportAgentChild::sendMsg, LocalTransportChild::sendMsgName());
m_child->activate();
// set up connection to parent
m_parent.reset(new LocalTransportParent(conn));
}
void onFailure(SyncMLStatus status, const std::string &reason)
{
SE_LOG_DEBUG(NULL, "child fork/exec failed: %s", reason.c_str());
// record failure for parent
if (!m_clientReport.getStatus()) {
m_clientReport.setStatus(status);
}
if (!reason.empty() &&
m_clientReport.getError().empty()) {
m_clientReport.setError(reason);
}
// return to step()
m_ret = 1;
}
// D-Bus API, see LocalTransportChild;
// must keep number of parameters < 9, the maximum supported by
// our D-Bus binding
void startSync(const std::string &clientConfig,
const StringPair &serverConfig, // config name + root path
const std::string &serverLogDir,
bool serverDoLogging,
const std::pair<UserIdentity, InitStateString> &serverSyncCredentials,
const FullProps &serverConfigProps,
const LocalTransportChild::ActiveSources_t &sources,
const LocalTransportChild::ReplyPtr &reply)
{
setMsgToParent(reply, "sync() was called");
string peer, context, normalConfig;
normalConfig = SyncConfig::normalizeConfigString(clientConfig);
SyncConfig::splitConfigString(normalConfig, peer, context);
if (peer.empty()) {
peer = "target-config";
}
// Keep the process name short in debug output if it is the
// normal "target-config", be more verbose if it is something
// else because it may be relevant.
if (peer != "target-config") {
Logger::setProcessName(peer + "@" + context);
} else {
Logger::setProcessName("@" + context);
}
SE_LOG_DEBUG(NULL, "Sync() called, starting the sync");
const char *delay = getenv("SYNCEVOLUTION_LOCAL_CHILD_DELAY2");
if (delay) {
Sleep(atoi(delay));
}
// initialize sync context
m_client.reset(new SyncContext(peer + "@" + context,
serverConfig.first,
serverConfig.second == "ephemeral" ?
serverConfig.second :
serverConfig.second + "/." + normalConfig,
boost::shared_ptr<TransportAgent>(this, NoopAgentDestructor()),
serverDoLogging));
if (serverConfig.second == "ephemeral") {
m_client->makeEphemeral();
}
boost::shared_ptr<UserInterface> ui(new LocalTransportUI(m_parent));
m_client->setUserInterface(ui);
// allow proceeding with sync even if no "target-config" was created,
// because information about username/password (for WebDAV) or the
// sources (for file backends) might be enough
m_client->setConfigNeeded(false);
// apply temporary config filters
m_client->setConfigFilter(true, "", serverConfigProps.createSyncFilter(m_client->getConfigName()));
BOOST_FOREACH(const string &sourceName, m_client->getSyncSources()) {
m_client->setConfigFilter(false, sourceName, serverConfigProps.createSourceFilter(m_client->getConfigName(), sourceName));
}
// Copy non-empty credentials from main config, because
// that is where the GUI knows how to store them. A better
// solution would be to require that credentials are in the
// "target-config" config.
//
// Interactive password requests later in SyncContext::sync()
// will end up in our LocalTransportContext::askPassword()
// implementation above, which will pass the question to
// the local sync parent.
if (!serverSyncCredentials.first.toString().empty()) {
m_client->setSyncUsername(serverSyncCredentials.first.toString(), true);
}
if (!serverSyncCredentials.second.empty()) {
m_client->setSyncPassword(serverSyncCredentials.second, true);
}
// debugging mode: write logs inside sub-directory of parent,
// otherwise use normal log settings
if (!serverDoLogging) {
m_client->setLogDir(std::string(serverLogDir) + "/child", true);
}
// disable all sources temporarily, will be enabled by next loop
BOOST_FOREACH(const string &targetName, m_client->getSyncSources()) {
SyncSourceNodes targetNodes = m_client->getSyncSourceNodes(targetName);
SyncSourceConfig targetSource(targetName, targetNodes);
targetSource.setSync("disabled", true);
}
// activate all sources in client targeted by main config,
// with right uri
BOOST_FOREACH(const LocalTransportChild::ActiveSources_t::value_type &entry, sources) {
// mapping is from server (source) to child (target)
const std::string &sourceName = entry.first;
const std::string &targetName = entry.second.first;
std::string sync = entry.second.second;
SyncMode mode = StringToSyncMode(sync);
if (mode != SYNC_NONE) {
SyncSourceNodes targetNodes = m_client->getSyncSourceNodes(targetName);
SyncSourceConfig targetSource(targetName, targetNodes);
string fullTargetName = normalConfig + "/" + targetName;
if (!targetNodes.dataConfigExists()) {
if (targetName.empty()) {
Exception::throwError(SE_HERE, "missing URI for one of the datastores");
} else {
Exception::throwError(SE_HERE, StringPrintf("%s: datastore not configured",
fullTargetName.c_str()));
}
}
// All of the config setting is done as volatile,
// so none of the regular config nodes have to
// be written. If a sync mode was set, it must have been
// done before in this loop => error in original config.
if (!targetSource.isDisabled()) {
Exception::throwError(SE_HERE,
StringPrintf("%s: datastore targetted twice by %s",
fullTargetName.c_str(),
serverConfig.first.c_str()));
}
// invert data direction
if (mode == SYNC_REFRESH_FROM_LOCAL) {
mode = SYNC_REFRESH_FROM_REMOTE;
} else if (mode == SYNC_REFRESH_FROM_REMOTE) {
mode = SYNC_REFRESH_FROM_LOCAL;
} else if (mode == SYNC_ONE_WAY_FROM_LOCAL) {
mode = SYNC_ONE_WAY_FROM_REMOTE;
} else if (mode == SYNC_ONE_WAY_FROM_REMOTE) {
mode = SYNC_ONE_WAY_FROM_LOCAL;
} else if (mode == SYNC_LOCAL_CACHE_SLOW) {
// Remote side is running in caching mode and
// asking for refresh. Send all our data.
mode = SYNC_SLOW;
} else if (mode == SYNC_LOCAL_CACHE_INCREMENTAL) {
// Remote side is running in caching mode and
// asking for an update. Use two-way mode although
// nothing is going to come back (simpler that way
// than using one-way, which has special code
// paths in libsynthesis).
mode = SYNC_TWO_WAY;
}
targetSource.setSync(PrettyPrintSyncMode(mode, true), true);
targetSource.setURI(sourceName, true);
}
}
// ready for m_client->sync()
m_status = ACTIVE;
}
void sendMsg(const std::string &contentType,
const GDBusCXX::DBusArray<uint8_t> &data,
const LocalTransportChild::ReplyPtr &reply)
{
SE_LOG_DEBUG(NULL, "child got message of %ld bytes", (long)data.first);
setMsgToParent(LocalTransportChild::ReplyPtr(), "sendMsg() was called");
if (m_status == ACTIVE) {
m_msgToParent = reply;
m_message.assign(reinterpret_cast<const char *>(data.second),
data.first);
m_messageType = contentType;
m_status = GOT_REPLY;
} else {
reply->failed(GDBusCXX::dbus_error("org.syncevolution.localtransport.error",
"child not expecting any message"));
}
}
// Must not be named setFreeze(), that is a virtual method in
// TransportAgent that we don't want to override!
void setFreezeLocalSync(bool freeze)
{
SE_LOG_DEBUG(NULL, "local transport child: setFreeze(%s)", freeze ? "true" : "false");
if (m_client) {
m_client->setFreeze(freeze);
}
}
public:
LocalTransportAgentChild() :
m_ret(0),
m_forkexec(SyncEvo::ForkExecChild::create()),
m_reportSent(false),
m_status(INACTIVE)
{
m_forkexec->m_onConnect.connect(boost::bind(&LocalTransportAgentChild::onConnect, this, _1));
m_forkexec->m_onFailure.connect(boost::bind(&LocalTransportAgentChild::onFailure, this, _1, _2));
// When parent quits, we need to abort whatever we do and shut
// down. There's no way how we can complete our work without it.
//
// Note that another way how this process can detect the
// death of the parent is when it currently is waiting for
// completion of a method call to the parent, like a request
// for a password. However, that does not cover failures
// like the parent not asking us to sync in the first place
// and also does not work with libdbus (https://bugs.freedesktop.org/show_bug.cgi?id=49728).
m_forkexec->m_onQuit.connect(&onParentQuit);
m_forkexec->connect();
}
boost::shared_ptr<ChildLogger> createLogger()
{
return boost::shared_ptr<ChildLogger>(new ChildLogger(m_child));
}
void run()
{
SuspendFlags &s = SuspendFlags::getSuspendFlags();
while (!m_parent) {
if (s.getState() != SuspendFlags::NORMAL) {
SE_LOG_DEBUG(NULL, "aborted, returning while waiting for parent");
return;
}
step("waiting for parent");
}
while (!m_client) {
if (s.getState() != SuspendFlags::NORMAL) {
SE_LOG_DEBUG(NULL, "aborted, returning while waiting for Sync() call from parent");
}
step("waiting for Sync() call from parent");
}
try {
// ignore SIGINT signal in local sync helper from now on:
// the parent process will handle those and tell us when
// we are expected to abort by sending a SIGTERM
struct sigaction new_action;
memset(&new_action, 0, sizeof(new_action));
new_action.sa_handler = SIG_IGN;
sigemptyset(&new_action.sa_mask);
sigaction(SIGINT, &new_action, NULL);
// SIGTERM would be caught by SuspendFlags and set the "abort"
// state. But a lot of code running in this process cannot
// check that flag in a timely manner (blocking calls in
// libneon, activesync client libraries, ...). Therefore
// it is better to abort inside the signal handler.
new_action.sa_handler = abortLocalSync;
sigaction(SIGTERM, &new_action, NULL);
SE_LOG_DEBUG(NULL, "LocalTransportChild: ignore SIGINT, die in SIGTERM");
SE_LOG_INFO(NULL, "target side of local sync ready");
m_client->sync(&m_clientReport);
} catch (...) {
string explanation;
SyncMLStatus status = Exception::handle(explanation);
m_clientReport.setStatus(status);
if (!explanation.empty() &&
m_clientReport.getError().empty()) {
m_clientReport.setError(explanation);
}
if (m_parent) {
std::string report = m_clientReport.toString();
SE_LOG_DEBUG(NULL, "child sending sync report after failure:\n%s", report.c_str());
m_parent->m_storeSyncReport.start(report,
boost::bind(&LocalTransportAgentChild::syncReportReceived, this, _1));
// wait for acknowledgement for report once:
// we are in some kind of error state, better
// do not wait too long
if (m_parent) {
SE_LOG_DEBUG(NULL, "waiting for parent's ACK for sync report");
g_main_context_iteration(NULL, true);
}
}
throw;
}
if (m_parent) {
// send final report, ignore result
std::string report = m_clientReport.toString();
SE_LOG_DEBUG(NULL, "child sending sync report:\n%s", report.c_str());
m_parent->m_storeSyncReport.start(report,
boost::bind(&LocalTransportAgentChild::syncReportReceived, this, _1));
while (!m_reportSent && m_parent &&
s.getState() == SuspendFlags::NORMAL) {
step("waiting for parent's ACK for sync report");
}
}
}
void syncReportReceived(const std::string &error)
{
SE_LOG_DEBUG(NULL, "sending sync report to parent: %s",
error.empty() ? "done" : error.c_str());
m_reportSent = true;
}
int getReturnCode() const { return m_ret; }
/**
* set transport specific URL of next message
*/
virtual void setURL(const std::string &url) {}
/**
* define content type for post, see content type constants
*/
virtual void setContentType(const std::string &type)
{
m_contentType = type;
}
/**
* Requests an normal shutdown of the transport. This can take a
* while, for example if communication is still pending.
* Therefore wait() has to be called to ensure that the
* shutdown is complete and that no error occurred.
*
* Simply deleting the transport is an *unnormal* shutdown that
* does not communicate with the peer.
*/
virtual void shutdown()
{
SE_LOG_DEBUG(NULL, "child local transport shutting down");
if (m_msgToParent) {
// Must send non-zero message, empty messages cause an
// error during D-Bus message decoding on the receiving
// side. Content doesn't matter, ignored by parent.
m_msgToParent->done("shutdown-message", GDBusCXX::makeDBusArray(1, (uint8_t *)""));
m_msgToParent.reset();
}
if (m_status != FAILED) {
m_status = CLOSED;
}
}
/**
* start sending message
*
* Memory must remain valid until reply is received or
* message transmission is canceled.
*
* @param data start address of data to send
* @param len number of bytes
*/
virtual void send(const char *data, size_t len)
{
SE_LOG_DEBUG(NULL, "child local transport sending %ld bytes", (long)len);
if (m_msgToParent) {
m_status = ACTIVE;
m_msgToParent->done(m_contentType, GDBusCXX::makeDBusArray(len, (uint8_t *)(data)));
m_msgToParent.reset();
} else {
m_status = FAILED;
SE_THROW("cannot send data to parent because parent is not waiting for message");
}
}
/**
* cancel an active message transmission
*
* Blocks until send buffer is no longer in use.
* Returns immediately if nothing pending.
*/
virtual void cancel() {}
/**
* Wait for completion of an operation initiated earlier.
* The operation can be a send with optional reply or
* a close request.
*
* Returns immediately if no operations is pending.
*
* @param noReply true if no reply is required for a running send;
* only relevant for transports used by a SyncML server
*/
virtual Status wait(bool noReply = false)
{
SuspendFlags &s = SuspendFlags::getSuspendFlags();
while (m_status == ACTIVE &&
s.getState() == SuspendFlags::NORMAL) {
step("waiting for next message");
}
return m_status;
}
/**
* Tells the transport agent to stop the transmission the given
* amount of seconds after send() was called. The transport agent
* will then stop the message transmission and return a TIME_OUT
* status in wait().
*
* @param seconds number of seconds to wait before timing out, zero for no timeout
*/
virtual void setTimeout(int seconds) {}
/**
* provides access to reply data
*
* Memory pointer remains valid as long as
* transport agent is not deleted and no other
* message is sent.
*/
virtual void getReply(const char *&data, size_t &len, std::string &contentType)
{
SE_LOG_DEBUG(NULL, "processing %ld bytes in child", (long)m_message.size());
if (m_status != GOT_REPLY) {
SE_THROW("getReply() called in child when no reply available");
}
data = m_message.c_str();
len = m_message.size();
contentType = m_messageType;
}
};
int LocalTransportMain(int argc, char **argv)
{
// delay the client for debugging purposes
const char *delay = getenv("SYNCEVOLUTION_LOCAL_CHILD_DELAY");
if (delay) {
Sleep(atoi(delay));
}
SyncContext::initMain("syncevo-local-sync");
// Our stderr is either connected to the original stderr (when
// SYNCEVOLUTION_DEBUG is set) or the local sync's parent
// LogRedirect. However, that stderr is not normally used.
// Instead we install our own LogRedirect for both stdout (for
// Execute() and synccompare, which then knows that it needs to
// capture the output) and stderr (to get output like the one from
// libneon into the child log) in LocalTransportAgentChild and
// send all logging output to the local sync parent via D-Bus, to
// be forwarded to the user as part of the normal message stream
// of the sync session.
setvbuf(stderr, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
// SIGPIPE must be ignored, some system libs (glib GIO?) trigger
// it. SIGINT/TERM will be handled via SuspendFlags once the sync
// runs.
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);
try {
if (getenv("SYNCEVOLUTION_DEBUG")) {
Logger::instance().setLevel(Logger::DEBUG);
}
// process name will be set to target config name once it is known
Logger::setProcessName("syncevo-local-sync");
boost::shared_ptr<LocalTransportAgentChild> child(new LocalTransportAgentChild);
PushLogger<Logger> logger;
// Temporary handle is necessary to avoid compiler issue with
// clang (ambiguous brackets).
{
Logger::Handle handle(child->createLogger());
logger.reset(handle);
}
#ifdef USE_DLT
// Set by syncevo-dbus-server for us.
bool useDLT = getenv("SYNCEVOLUTION_USE_DLT") != NULL;
PushLogger<LoggerDLT> loggerdlt;
if (useDLT) {
loggerdlt.reset(new LoggerDLT(DLT_SYNCEVO_LOCAL_HELPER_ID, "SyncEvolution local sync helper"));
}
#endif
child->run();
int ret = child->getReturnCode();
logger.reset();
child.reset();
return ret;
} catch ( const std::exception &ex ) {
SE_LOG_ERROR(NULL, "%s", ex.what());
} catch (...) {
SE_LOG_ERROR(NULL, "unknown error");
}
return 1;
}
SE_END_CXX