/* * Copyright (C) 2005-2008 Patrick Ohly */ #include "EvolutionSyncClient.h" #include "EvolutionSyncSource.h" #include "SyncEvolutionUtil.h" #include #include #include #include #include #include #include #include #include using namespace std; #include #include #include #include #include #include #include #include SourceList *EvolutionSyncClient::m_sourceListPtr; EvolutionSyncClient::EvolutionSyncClient(const string &server, bool doLogging, const set &sources) : EvolutionSyncConfig(server), m_server(server), m_sources(sources), m_doLogging(doLogging), m_syncMode(SYNC_NONE), m_quiet(false) { } EvolutionSyncClient::~EvolutionSyncClient() { } // this class owns the logging directory and is responsible // for redirecting output at the start and end of sync (even // in case of exceptions thrown!) class LogDir { string m_logdir; /**< configured backup root dir, empty if none */ int m_maxlogdirs; /**< number of backup dirs to preserve, 0 if unlimited */ string m_prefix; /**< common prefix of backup dirs */ string m_path; /**< path to current logging and backup dir */ string m_logfile; /**< path to log file there, empty if not writing one */ const string &m_server; /**< name of the server for this synchronization */ LogLevel m_oldLogLevel; /**< logging level to restore */ bool m_restoreLog; /**< false if nothing needs to be restored because setLogdir() was never called */ public: LogDir(const string &server) : m_server(server), m_restoreLog(false) { // SyncEvolution----
-- m_prefix = "SyncEvolution-"; m_prefix += m_server; // default: $TMPDIR/SyncEvolution-- stringstream path; char *tmp = getenv("TMPDIR"); if (tmp) { path << tmp; } else { path << "/tmp"; } path << "/SyncEvolution-"; struct passwd *user = getpwuid(getuid()); if (user && user->pw_name) { path << user->pw_name; } else { path << getuid(); } path << "-" << m_server; m_path = path.str(); } /** * Finds previous log directory. Must be called before setLogdir(). * * @param path path to configured backup directy, NULL if defaulting to /tmp, "none" if not creating log file * @return full path of previous log directory, empty string if not found */ string previousLogdir(const char *path) { string logdir; if (path && !strcasecmp(path, "none")) { return ""; } else if (path && path[0]) { vector entries; try { getLogdirs(path, entries); } catch(const std::exception &ex) { LOG.error("%s", ex.what()); return ""; } logdir = entries.size() ? string(path) + "/" + entries[entries.size()-1] : ""; } else { logdir = m_path; } if (access(logdir.c_str(), R_OK|X_OK) == 0) { return logdir; } else { return ""; } } // setup log directory and redirect logging into it // @param path path to configured backup directy, NULL if defaulting to /tmp, "none" if not creating log file // @param maxlogdirs number of backup dirs to preserve in path, 0 if unlimited // @param logLevel 0 = default, 1 = ERROR, 2 = INFO, 3 = DEBUG void setLogdir(const char *path, int maxlogdirs, int logLevel = 0) { m_maxlogdirs = maxlogdirs; if (path && !strcasecmp(path, "none")) { m_logfile = ""; } else if (path && path[0]) { m_logdir = path; // create unique directory name in the given directory time_t ts = time(NULL); struct tm *tm = localtime(&ts); stringstream base; base << path << "/" << m_prefix << "-" << setfill('0') << setw(4) << tm->tm_year + 1900 << "-" << setw(2) << tm->tm_mon + 1 << "-" << setw(2) << tm->tm_mday << "-" << setw(2) << tm->tm_hour << "-" << setw(2) << tm->tm_min; int seq = 0; while (true) { stringstream path; path << base.str(); if (seq) { path << "-" << seq; } m_path = path.str(); if (!mkdir(m_path.c_str(), S_IRWXU)) { break; } if (errno != EEXIST) { LOG.debug("%s: %s", m_path.c_str(), strerror(errno)); EvolutionSyncClient::throwError(m_path, errno); } seq++; } m_logfile = m_path + "/client.log"; } else { // use the default temp directory if (mkdir(m_path.c_str(), S_IRWXU)) { if (errno != EEXIST) { EvolutionSyncClient::throwError(m_path, errno); } } m_logfile = m_path + "/client.log"; } if (m_logfile.size()) { // redirect logging into that directory, including stderr, // after truncating it FILE *file = fopen(m_logfile.c_str(), "w"); if (file) { fclose(file); #ifdef POSIX_LOG POSIX_LOG. #endif setLogFile(NULL, m_logfile.c_str(), true); } else { LOG.error("creating log file %s failed", m_logfile.c_str()); } } m_oldLogLevel = LOG.getLevel(); LOG.setLevel(logLevel > 0 ? (LogLevel)(logLevel - 1) /* fixed level */ : m_logfile.size() ? LOG_LEVEL_DEBUG /* default for log file */ : LOG_LEVEL_INFO /* default for console output */ ); m_restoreLog = true; } /** sets a fixed directory for database files without redirecting logging */ void setPath(const string &path) { m_path = path; } // return log directory, empty if not enabled const string &getLogdir() { return m_path; } // return log file, empty if not enabled const string &getLogfile() { return m_logfile; } /** find all entries in a given directory, return as sorted array */ void getLogdirs(const string &logdir, vector &entries) { ReadDir dir(logdir); BOOST_FOREACH(const string &entry, dir) { if (boost::starts_with(entry, m_prefix)) { entries.push_back(entry); } } sort(entries.begin(), entries.end()); } // remove oldest backup dirs if exceeding limit void expire() { if (m_logdir.size() && m_maxlogdirs > 0 ) { vector entries; getLogdirs(m_logdir, entries); int deleted = 0; for (vector::iterator it = entries.begin(); it != entries.end() && (int)entries.size() - deleted > m_maxlogdirs; ++it, ++deleted) { string path = m_logdir + "/" + *it; string msg = "removing " + path; LOG.info(msg.c_str()); rm_r(path); } } } // remove redirection of stderr and (optionally) also of logging void restore(bool all) { if (!m_restoreLog) { return; } if (all) { if (m_logfile.size()) { #ifdef POSIX_LOG POSIX_LOG. #endif setLogFile(NULL, "-", false); } LOG.setLevel(m_oldLogLevel); } else { if (m_logfile.size()) { #ifdef POSIX_LOG POSIX_LOG. #endif setLogFile(NULL, m_logfile.c_str(), false); } } } ~LogDir() { restore(true); } }; // this class owns the sync sources and (together with // a logdir) handles writing of per-sync files as well // as the final report ( class SourceList : public vector { LogDir m_logdir; /**< our logging directory */ bool m_prepared; /**< remember whether syncPrepare() dumped databases successfully */ bool m_doLogging; /**< true iff additional files are to be written during sync */ SyncClient &m_client; /**< client which holds the sync report after a sync */ bool m_reportTodo; /**< true if syncDone() shall print a final report */ boost::scoped_array m_sourceArray; /** owns the array that is expected by SyncClient::sync() */ const bool m_quiet; /**< avoid redundant printing to screen */ string m_previousLogdir; /**< remember previous log dir before creating the new one */ /** create name in current (if set) or previous logdir */ string databaseName(EvolutionSyncSource &source, const string suffix, string logdir = "") { if (!logdir.size()) { logdir = m_logdir.getLogdir(); } return logdir + "/" + source.getName() + "." + suffix + "." + source.fileSuffix(); } public: /** * dump into files with a certain suffix */ void dumpDatabases(const string &suffix) { ofstream out; #ifndef IPHONE // output stream on iPhone raises exception even though it is in a good state; // perhaps the missing C++ exception support is the reason: // http://code.google.com/p/iphone-dev/issues/detail?id=48 out.exceptions(ios_base::badbit|ios_base::failbit|ios_base::eofbit); #endif BOOST_FOREACH(EvolutionSyncSource *source, *this) { string file = databaseName(*source, suffix); LOG.debug("creating %s", file.c_str()); out.open(file.c_str()); source->exportData(out); out.close(); LOG.debug("%s created", file.c_str()); } } /** remove database dumps with a specific suffix */ void removeDatabases(const string &removeSuffix) { BOOST_FOREACH(EvolutionSyncSource *source, *this) { string file; file = databaseName(*source, removeSuffix); unlink(file.c_str()); } } SourceList(const string &server, bool doLogging, SyncClient &client, bool quiet) : m_logdir(server), m_prepared(false), m_doLogging(doLogging), m_client(client), m_reportTodo(true), m_quiet(quiet) { } // call as soon as logdir settings are known void setLogdir(const char *logDirPath, int maxlogdirs, int logLevel) { m_previousLogdir = m_logdir.previousLogdir(logDirPath); if (m_doLogging) { m_logdir.setLogdir(logDirPath, maxlogdirs, logLevel); } else { // at least increase log level LOG.setLevel(LOG_LEVEL_DEBUG); } } /** return previous log dir found in setLogdir() */ const string &getPrevLogdir() const { return m_previousLogdir; } /** set directory for database files without actually redirecting the logging */ void setPath(const string &path) { m_logdir.setPath(path); } /** * If possible (m_previousLogdir found) and enabled (!m_quiet), * then dump changes applied locally. * * @param oldSuffix suffix of old database dump: usually "after" * @param currentSuffix the current database dump suffix: "current" * when not doing a sync, otherwise "before" */ bool dumpLocalChanges(const string &oldSuffix, const string &newSuffix) { if (m_quiet || !m_previousLogdir.size()) { return false; } cout << "Local changes to be applied to server during synchronization:\n"; BOOST_FOREACH(EvolutionSyncSource *source, *this) { string oldFile = databaseName(*source, oldSuffix, m_previousLogdir); string newFile = databaseName(*source, newSuffix); cout << "*** " << source->getName() << " ***\n" << flush; string cmd = string("env CLIENT_TEST_COMPARISON_FAILED=10 CLIENT_TEST_LEFT_NAME='after last sync' CLIENT_TEST_RIGHT_NAME='current data' CLIENT_TEST_REMOVED='removed since last sync' CLIENT_TEST_ADDED='added since last sync' synccompare 2>/dev/null '" ) + oldFile + "' '" + newFile + "'"; int ret = system(cmd.c_str()); switch (ret == -1 ? ret : WEXITSTATUS(ret)) { case 0: cout << "no changes\n"; break; case 10: break; default: cout << "Comparison was impossible.\n"; break; } } cout << "\n"; return true; } // call when all sync sources are ready to dump // pre-sync databases void syncPrepare() { if (m_logdir.getLogfile().size() && m_doLogging) { // dump initial databases dumpDatabases("before"); // compare against the old "after" database dump dumpLocalChanges("after", "before"); // now remove the old database dump removeDatabases("after"); m_prepared = true; } } // call at the end of a sync with success == true // if all went well to print report void syncDone(bool success) { if (m_doLogging) { // ensure that stderr is seen again m_logdir.restore(false); if (m_reportTodo) { // haven't looked at result of sync yet; // don't do it again m_reportTodo = false; // dump datatbase after sync, but not if already dumping it at the beginning didn't complete if (m_prepared) { try { dumpDatabases("after"); } catch (const std::exception &ex) { LOG.error( "%s", ex.what() ); m_prepared = false; } } string logfile = m_logdir.getLogfile(); #ifndef LOG_HAVE_SET_LOGGER // scan for error messages if (!m_quiet && logfile.size()) { ifstream in; in.open(m_logdir.getLogfile().c_str()); while (in.good()) { string line; getline(in, line); if (line.find("[ERROR]") != line.npos) { success = false; cout << line << "\n"; } else if (line.find("[INFO]") != line.npos) { cout << line << "\n"; } } in.close(); } #endif cout << flush; cerr << flush; cout << "\n"; if (success) { cout << "Synchronization successful.\n"; } else if (logfile.size()) { cout << "Synchronization failed, see " << logfile << " for details.\n"; } else { cout << "Synchronization failed.\n"; } // pretty-print report if (!m_quiet) { cout << "\nChanges applied during synchronization:\n"; } SyncReport *report = m_client.getSyncReport(); if (!m_quiet && report) { cout << "+-------------------|-------ON CLIENT-------|-------ON SERVER-------|\n"; cout << "| | successful / total | successful / total |\n"; cout << "| Source | NEW | MOD | DEL | NEW | MOD | DEL |\n"; const char *sep = "+-------------------+-------+-------+-------+-------+-------+-------+\n"; cout << sep; for (unsigned int i = 0; report->getSyncSourceReport(i); i++) { SyncSourceReport* ssr = report->getSyncSourceReport(i); if (ssr->getState() == SOURCE_INACTIVE) { continue; } cout << "|" << right << setw(18) << ssr->getSourceName() << " |"; static const char * const targets[] = { CLIENT, SERVER, NULL }; for (int target = 0; targets[target]; target++) { static const char * const commands[] = { COMMAND_ADD, COMMAND_REPLACE, COMMAND_DELETE, NULL }; for (int command = 0; commands[command]; command++) { cout << right << setw(3) << ssr->getItemReportSuccessfulCount(targets[target], commands[command]); cout << "/"; cout << left << setw(3) << ssr->getItemReportCount(targets[target], commands[command]); cout << "|"; } } cout << "\n"; } cout << sep; } // compare databases? if (!m_quiet && m_prepared) { cout << "\nChanges applied to client during synchronization:\n"; BOOST_FOREACH(EvolutionSyncSource *source, *this) { cout << "*** " << source->getName() << " ***\n" << flush; string before = databaseName(*source, "before"); string after = databaseName(*source, "after"); string cmd = string("synccompare '" ) + before + "' '" + after + "' && echo 'no changes'"; system(cmd.c_str()); } cout << "\n"; } if (success) { m_logdir.expire(); } } } } /** returns current sources as array as expected by SyncClient::sync(), memory owned by this class */ SyncSource **getSourceArray() { m_sourceArray.reset(new SyncSource *[size() + 1]); int index = 0; BOOST_FOREACH(EvolutionSyncSource *source, *this) { m_sourceArray[index] = source; index++; } m_sourceArray[index] = 0; return &m_sourceArray[0]; } /** returns names of active sources */ set getSources() { set res; BOOST_FOREACH(EvolutionSyncSource *source, *this) { res.insert(source->getName()); } return res; } ~SourceList() { // free sync sources BOOST_FOREACH(EvolutionSyncSource *source, *this) { delete source; } } /** find sync source by name */ EvolutionSyncSource *operator [] (const string &name) { BOOST_FOREACH(EvolutionSyncSource *source, *this) { if (name == source->getName()) { return source; } } return NULL; } /** find by index */ EvolutionSyncSource *operator [] (int index) { return vector::operator [] (index); } }; void unref(SourceList *sourceList) { delete sourceList; } string EvolutionSyncClient::askPassword(const string &descr) { char buffer[256]; printf("Enter password for %s: ", descr.c_str()); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) && strcmp(buffer, "\n")) { size_t len = strlen(buffer); if (len && buffer[len - 1] == '\n') { buffer[len - 1] = 0; } return buffer; } else { throwError(string("could not read password for ") + descr); return ""; } } void EvolutionSyncClient::throwError(const string &error) { #ifdef IPHONE /* * Catching the runtime_exception fails due to a toolchain problem, * so do the error handling now and abort: because there is just * one sync source this is probably the only thing that can be done. * Still, it's not nice on the server... */ fatalError(NULL, error.c_str()); #else throw runtime_error(error); #endif } void EvolutionSyncClient::throwError(const string &action, int error) { throwError(action + ": " + strerror(error)); } void EvolutionSyncClient::fatalError(void *object, const char *error) { LOG.error("%s", error); if (m_sourceListPtr) { m_sourceListPtr->syncDone(false); } exit(1); } /* * There have been segfaults inside glib in the background * thread which ran the second event loop. Disabled it again, * even though the synchronous EDS API calls will block then * when EDS dies. */ #if 0 && defined(HAVE_GLIB) && defined(HAVE_EDS) # define RUN_GLIB_LOOP #endif #ifdef RUN_GLIB_LOOP #include #include static void *mainLoopThread(void *) { // The test framework uses SIGALRM for timeouts. // Block the signal here because a) the signal handler // prints a stack back trace when called and we are not // interessted in the background thread's stack and b) // it seems to have confused glib/libebook enough to // access invalid memory and segfault when it gets the SIGALRM. sigset_t blocked; sigemptyset(&blocked); sigaddset(&blocked, SIGALRM); pthread_sigmask(SIG_BLOCK, &blocked, NULL); GMainLoop *mainloop = g_main_loop_new(NULL, TRUE); if (mainloop) { g_main_loop_run(mainloop); g_main_loop_unref(mainloop); } return NULL; } #endif void EvolutionSyncClient::startLoopThread() { #ifdef RUN_GLIB_LOOP // when using Evolution we must have a running main loop, // otherwise loss of connection won't be reported to us static pthread_t loopthread; static bool loopthreadrunning; if (!loopthreadrunning) { loopthreadrunning = !pthread_create(&loopthread, NULL, mainLoopThread, NULL); } #endif } AbstractSyncSourceConfig* EvolutionSyncClient::getAbstractSyncSourceConfig(const char* name) const { return m_sourceListPtr ? (*m_sourceListPtr)[name] : NULL; } AbstractSyncSourceConfig* EvolutionSyncClient::getAbstractSyncSourceConfig(unsigned int i) const { return m_sourceListPtr ? (*m_sourceListPtr)[i] : NULL; } unsigned int EvolutionSyncClient::getAbstractSyncSourceConfigsCount() const { return m_sourceListPtr ? m_sourceListPtr->size() : 0; } void EvolutionSyncClient::setConfigFilter(bool sync, const FilterConfigNode::ConfigFilter &filter) { map::const_iterator hasSync = filter.find(EvolutionSyncSourceConfig::m_sourcePropSync.getName()); if (!sync && hasSync != filter.end()) { m_overrideMode = hasSync->second; FilterConfigNode::ConfigFilter strippedFilter = filter; strippedFilter.erase(EvolutionSyncSourceConfig::m_sourcePropSync.getName()); EvolutionSyncConfig::setConfigFilter(sync, strippedFilter); } else { EvolutionSyncConfig::setConfigFilter(sync, filter); } } void EvolutionSyncClient::initSources(SourceList &sourceList) { set unmatchedSources = m_sources; list configuredSources = getSyncSources(); BOOST_FOREACH(const string &name, configuredSources) { boost::shared_ptr sc(getSyncSourceConfig(name)); // is the source enabled? string sync = sc->getSync(); bool enabled = sync != "disabled"; string overrideMode = m_overrideMode; // override state? if (m_sources.size()) { if (m_sources.find(sc->getName()) != m_sources.end()) { if (!enabled) { if (overrideMode.empty()) { overrideMode = "two-way"; } enabled = true; } unmatchedSources.erase(sc->getName()); } else { enabled = false; } } if (enabled) { string url = getSyncURL(); boost::replace_first(url, "https://", "http://"); // do not distinguish between protocol in change tracking string changeId = string("sync4jevolution:") + url + "/" + name; EvolutionSyncSourceParams params(name, getSyncSourceNodes(name), changeId); // the sync mode has to be set before instantiating the source // because the client library reads the preferredSyncMode at that time if (!overrideMode.empty()) { params.m_nodes.m_configNode->addFilter(EvolutionSyncSourceConfig::m_sourcePropSync.getName(), overrideMode); } EvolutionSyncSource *syncSource = EvolutionSyncSource::createSource(params); if (!syncSource) { throwError(name + ": type unknown" ); } sourceList.push_back(syncSource); } } // check whether there were any sources specified which do not exist if (unmatchedSources.size()) { throwError(string("no such source(s): ") + boost::join(unmatchedSources, " ")); } } int EvolutionSyncClient::sync() { int res = 1; if (!exists()) { LOG.error("No configuration for server \"%s\" found.", m_server.c_str()); throwError("cannot proceed without configuration"); } // redirect logging as soon as possible SourceList sourceList(m_server, m_doLogging, *this, m_quiet); m_sourceListPtr = &sourceList; try { sourceList.setLogdir(getLogDir(), getMaxLogDirs(), getLogLevel()); // dump some summary information at the beginning of the log #ifdef LOG_HAVE_DEVELOPER # define LOG_DEVELOPER developer #else # define LOG_DEVELOPER debug #endif LOG.LOG_DEVELOPER("SyncML server account: %s", getUsername()); LOG.LOG_DEVELOPER("client: SyncEvolution %s for %s", getSwv(), getDevType()); LOG.LOG_DEVELOPER("device ID: %s", getDevID()); LOG.LOG_DEVELOPER("%s", EDSAbiWrapperDebug()); // instantiate backends, but do not open them yet initSources(sourceList); // request all config properties once: throwing exceptions // now is okay, whereas later it would lead to leaks in the // not exception safe client library EvolutionSyncConfig dummy; set activeSources = sourceList.getSources(); dummy.copy(*this, &activeSources); // start background thread if not running yet: // necessary to catch problems with Evolution backend startLoopThread(); // ask for passwords now checkPassword(*this); if (getUseProxy()) { checkProxyPassword(*this); } BOOST_FOREACH(EvolutionSyncSource *source, sourceList) { source->checkPassword(*this); } // open each source - failing now is still safe BOOST_FOREACH(EvolutionSyncSource *source, sourceList) { source->open(); } // give derived class also a chance to update the configs prepare(sourceList.getSourceArray()); // ready to go: dump initial databases and prepare for final report sourceList.syncPrepare(); // do it res = SyncClient::sync(*this, sourceList.getSourceArray()); // store modified properties: must be done even after failed // sync because the source's anchor might have been reset flush(); if (res) { if (getLastErrorCode() && getLastErrorMsg() && getLastErrorMsg()[0]) { throwError(getLastErrorMsg()); } // no error code/description?! throwError("sync failed without an error description, check log"); } // all went well: print final report before cleaning up sourceList.syncDone(true); res = 0; } catch (const std::exception &ex) { LOG.error( "%s", ex.what() ); // something went wrong, but try to write .after state anyway m_sourceListPtr = NULL; sourceList.syncDone(false); } catch (...) { LOG.error( "unknown error" ); m_sourceListPtr = NULL; sourceList.syncDone(false); } m_sourceListPtr = NULL; return res; } void EvolutionSyncClient::prepare(SyncSource **sources) { if (m_syncMode != SYNC_NONE) { for (SyncSource **source = sources; *source; source++) { (*source)->setPreferredSyncMode(m_syncMode); } } } void EvolutionSyncClient::status() { EvolutionSyncConfig config(m_server); if (!exists()) { LOG.error("No configuration for server \"%s\" found.", m_server.c_str()); throwError("cannot proceed without configuration"); } SourceList sourceList(m_server, false, *this, false); initSources(sourceList); BOOST_FOREACH(EvolutionSyncSource *source, sourceList) { source->checkPassword(*this); } BOOST_FOREACH(EvolutionSyncSource *source, sourceList) { source->open(); } sourceList.setLogdir(getLogDir(), 0, LOG_LEVEL_NONE); LOG.setLevel(LOG_LEVEL_INFO); string prevLogdir = sourceList.getPrevLogdir(); bool found = access(prevLogdir.c_str(), R_OK|X_OK) == 0; if (found) { try { sourceList.setPath(prevLogdir); sourceList.dumpDatabases("current"); sourceList.dumpLocalChanges("after", "current"); } catch(const std::exception &ex) { LOG.error("%s", ex.what()); } } else { cerr << "Previous log directory not found.\n"; if (!getLogDir() || !getLogDir()[0]) { cerr << "Enable the 'logdir' option and synchronize to use this feature.\n"; } } }