2 * Copyright (C) 2005-2006 Patrick Ohly
3 * Copyright (C) 2007 Funambol
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 #include "EvolutionSyncClient.h"
21 #include "EvolutionSyncSource.h"
22 #include "SyncEvolutionUtil.h"
24 #include <posix/base/posixlog.h>
36 #include <boost/algorithm/string/predicate.hpp>
37 #include <boost/algorithm/string/replace.hpp>
38 #include <boost/foreach.hpp>
46 SourceList *EvolutionSyncClient::m_sourceListPtr;
48 EvolutionSyncClient::EvolutionSyncClient(const string &server,
50 const set<string> &sources) :
51 EvolutionSyncConfig(server),
54 m_doLogging(doLogging),
55 m_syncMode(SYNC_NONE),
60 EvolutionSyncClient::~EvolutionSyncClient()
65 // this class owns the logging directory and is responsible
66 // for redirecting output at the start and end of sync (even
67 // in case of exceptions thrown!)
69 string m_logdir; /**< configured backup root dir, empty if none */
70 int m_maxlogdirs; /**< number of backup dirs to preserve, 0 if unlimited */
71 string m_prefix; /**< common prefix of backup dirs */
72 string m_path; /**< path to current logging and backup dir */
73 string m_logfile; /**< path to log file there, empty if not writing one */
74 const string &m_server; /**< name of the server for this synchronization */
75 LogLevel m_oldLogLevel; /**< logging level to restore */
76 bool m_restoreLog; /**< false if nothing needs to be restored because setLogdir() was never called */
79 LogDir(const string &server) : m_server(server),
82 // SyncEvolution-<server>-<yyyy>-<mm>-<dd>-<hh>-<mm>
83 m_prefix = "SyncEvolution-";
86 // default: $TMPDIR/SyncEvolution-<username>-<server>
88 char *tmp = getenv("TMPDIR");
94 path << "/SyncEvolution-";
95 struct passwd *user = getpwuid(getuid());
96 if (user && user->pw_name) {
97 path << user->pw_name;
101 path << "-" << m_server;
107 * Finds previous log directory. Must be called before setLogdir().
109 * @param path path to configured backup directy, NULL if defaulting to /tmp, "none" if not creating log file
110 * @return full path of previous log directory, empty string if not found
112 string previousLogdir(const char *path) {
115 if (path && !strcasecmp(path, "none")) {
117 } else if (path && path[0]) {
118 vector<string> entries;
120 getLogdirs(path, entries);
121 } catch(const std::exception &ex) {
122 LOG.error("%s", ex.what());
126 logdir = entries.size() ? string(path) + "/" + entries[entries.size()-1] : "";
131 if (access(logdir.c_str(), R_OK|X_OK) == 0) {
138 // setup log directory and redirect logging into it
139 // @param path path to configured backup directy, NULL if defaulting to /tmp, "none" if not creating log file
140 // @param maxlogdirs number of backup dirs to preserve in path, 0 if unlimited
141 // @param logLevel 0 = default, 1 = ERROR, 2 = INFO, 3 = DEBUG
142 void setLogdir(const char *path, int maxlogdirs, int logLevel = 0) {
143 m_maxlogdirs = maxlogdirs;
144 if (path && !strcasecmp(path, "none")) {
146 } else if (path && path[0]) {
149 // create unique directory name in the given directory
150 time_t ts = time(NULL);
151 struct tm *tm = localtime(&ts);
157 << setw(4) << tm->tm_year + 1900 << "-"
158 << setw(2) << tm->tm_mon + 1 << "-"
159 << setw(2) << tm->tm_mday << "-"
160 << setw(2) << tm->tm_hour << "-"
161 << setw(2) << tm->tm_min;
170 if (!mkdir(m_path.c_str(), S_IRWXU)) {
173 if (errno != EEXIST) {
174 LOG.debug("%s: %s", m_path.c_str(), strerror(errno));
175 EvolutionSyncClient::throwError(m_path + ": " + strerror(errno));
179 m_logfile = m_path + "/client.log";
181 // use the default temp directory
182 if (mkdir(m_path.c_str(), S_IRWXU)) {
183 if (errno != EEXIST) {
184 EvolutionSyncClient::throwError(m_path + ": " + strerror(errno));
187 m_logfile = m_path + "/client.log";
190 if (m_logfile.size()) {
191 // redirect logging into that directory, including stderr,
192 // after truncating it
193 FILE *file = fopen(m_logfile.c_str(), "w");
199 setLogFile(NULL, m_logfile.c_str(), true);
201 LOG.error("creating log file %s failed", m_logfile.c_str());
204 m_oldLogLevel = LOG.getLevel();
205 LOG.setLevel(logLevel > 0 ? (LogLevel)(logLevel - 1) /* fixed level */ :
206 m_logfile.size() ? LOG_LEVEL_DEBUG /* default for log file */ :
207 LOG_LEVEL_INFO /* default for console output */ );
211 /** sets a fixed directory for database files without redirecting logging */
212 void setPath(const string &path) { m_path = path; }
214 // return log directory, empty if not enabled
215 const string &getLogdir() {
219 // return log file, empty if not enabled
220 const string &getLogfile() {
224 /** find all entries in a given directory, return as sorted array */
225 void getLogdirs(const string &logdir, vector<string> &entries) {
227 for (ReadDir::const_iterator it = dir.begin();
230 if (boost::starts_with(*it, m_prefix)) {
231 entries.push_back(*it);
234 sort(entries.begin(), entries.end());
238 // remove oldest backup dirs if exceeding limit
240 if (m_logdir.size() && m_maxlogdirs > 0 ) {
241 vector<string> entries;
242 getLogdirs(m_logdir, entries);
245 for (vector<string>::iterator it = entries.begin();
246 it != entries.end() && (int)entries.size() - deleted > m_maxlogdirs;
248 string path = m_logdir + "/" + *it;
249 string msg = "removing " + path;
250 LOG.info(msg.c_str());
256 // remove redirection of stderr and (optionally) also of logging
257 void restore(bool all) {
263 if (m_logfile.size()) {
267 setLogFile(NULL, "-", false);
269 LOG.setLevel(m_oldLogLevel);
271 if (m_logfile.size()) {
275 setLogFile(NULL, m_logfile.c_str(), false);
285 // this class owns the sync sources and (together with
286 // a logdir) handles writing of per-sync files as well
287 // as the final report (
288 class SourceList : public vector<EvolutionSyncSource *> {
289 LogDir m_logdir; /**< our logging directory */
290 bool m_prepared; /**< remember whether syncPrepare() dumped databases successfully */
291 bool m_doLogging; /**< true iff additional files are to be written during sync */
292 SyncClient &m_client; /**< client which holds the sync report after a sync */
293 bool m_reportTodo; /**< true if syncDone() shall print a final report */
294 boost::scoped_array<SyncSource *> m_sourceArray; /** owns the array that is expected by SyncClient::sync() */
295 const bool m_quiet; /**< avoid redundant printing to screen */
296 string m_previousLogdir; /**< remember previous log dir before creating the new one */
298 /** create name in current (if set) or previous logdir */
299 string databaseName(EvolutionSyncSource &source, const string suffix, string logdir = "") {
300 if (!logdir.size()) {
301 logdir = m_logdir.getLogdir();
303 return logdir + "/" +
304 source.getName() + "." + suffix + "." +
310 * dump into files with a certain suffix
312 void dumpDatabases(const string &suffix) {
315 // output stream on iPhone raises exception even though it is in a good state;
316 // perhaps the missing C++ exception support is the reason:
317 // http://code.google.com/p/iphone-dev/issues/detail?id=48
318 out.exceptions(ios_base::badbit|ios_base::failbit|ios_base::eofbit);
321 for( iterator it = begin();
324 string file = databaseName(**it, suffix);
325 LOG.debug("creating %s", file.c_str());
326 out.open(file.c_str());
327 (*it)->exportData(out);
329 LOG.debug("%s created", file.c_str());
333 /** remove database dumps with a specific suffix */
334 void removeDatabases(const string &removeSuffix) {
335 for( iterator it = begin();
340 file = databaseName(**it, removeSuffix);
341 unlink(file.c_str());
345 SourceList(const string &server, bool doLogging, SyncClient &client, bool quiet) :
348 m_doLogging(doLogging),
355 // call as soon as logdir settings are known
356 void setLogdir(const char *logDirPath, int maxlogdirs, int logLevel) {
357 m_previousLogdir = m_logdir.previousLogdir(logDirPath);
359 m_logdir.setLogdir(logDirPath, maxlogdirs, logLevel);
361 // at least increase log level
362 LOG.setLevel(LOG_LEVEL_DEBUG);
366 /** return previous log dir found in setLogdir() */
367 const string &getPrevLogdir() const { return m_previousLogdir; }
369 /** set directory for database files without actually redirecting the logging */
370 void setPath(const string &path) { m_logdir.setPath(path); }
373 * If possible (m_previousLogdir found) and enabled (!m_quiet),
374 * then dump changes applied locally.
376 * @param oldSuffix suffix of old database dump: usually "after"
377 * @param currentSuffix the current database dump suffix: "current"
378 * when not doing a sync, otherwise "before"
380 bool dumpLocalChanges(const string &oldSuffix, const string &newSuffix) {
381 if (m_quiet || !m_previousLogdir.size()) {
385 cout << "Local changes to be applied to server during synchronization:\n";
386 for( iterator it = begin();
389 string oldFile = databaseName(**it, oldSuffix, m_previousLogdir);
390 string newFile = databaseName(**it, newSuffix);
391 cout << "*** " << (*it)->getName() << " ***\n" << flush;
392 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 '" ) +
393 oldFile + "' '" + newFile + "'";
394 int ret = system(cmd.c_str());
395 switch (ret == -1 ? ret : WEXITSTATUS(ret)) {
397 cout << "no changes\n";
402 cout << "Comparison was impossible.\n";
410 // call when all sync sources are ready to dump
411 // pre-sync databases
413 if (m_logdir.getLogfile().size() &&
415 // dump initial databases
416 dumpDatabases("before");
417 // compare against the old "after" database dump
418 dumpLocalChanges("after", "before");
419 // now remove the old database dump
420 removeDatabases("after");
425 // call at the end of a sync with success == true
426 // if all went well to print report
427 void syncDone(bool success) {
429 // ensure that stderr is seen again
430 m_logdir.restore(false);
433 // haven't looked at result of sync yet;
435 m_reportTodo = false;
437 // dump datatbase after sync, but not if already dumping it at the beginning didn't complete
440 dumpDatabases("after");
441 } catch (const std::exception &ex) {
442 LOG.error( "%s", ex.what() );
447 string logfile = m_logdir.getLogfile();
448 #ifndef LOG_HAVE_SET_LOGGER
449 // scan for error messages
450 if (!m_quiet && logfile.size()) {
452 in.open(m_logdir.getLogfile().c_str());
456 if (line.find("[ERROR]") != line.npos) {
458 cout << line << "\n";
459 } else if (line.find("[INFO]") != line.npos) {
460 cout << line << "\n";
471 cout << "Synchronization successful.\n";
472 } else if (logfile.size()) {
473 cout << "Synchronization failed, see "
475 << " for details.\n";
477 cout << "Synchronization failed.\n";
480 // pretty-print report
482 cout << "\nChanges applied during synchronization:\n";
484 SyncReport *report = m_client.getSyncReport();
485 if (!m_quiet && report) {
487 cout << "+-------------------|-------ON CLIENT-------|-------ON SERVER-------|\n";
488 cout << "| | successful / total | successful / total |\n";
489 cout << "| Source | NEW | MOD | DEL | NEW | MOD | DEL |\n";
491 "+-------------------+-------+-------+-------+-------+-------+-------+\n";
494 for (unsigned int i = 0; report->getSyncSourceReport(i); i++) {
495 SyncSourceReport* ssr = report->getSyncSourceReport(i);
497 if (ssr->getState() == SOURCE_INACTIVE) {
501 cout << "|" << right << setw(18) << ssr->getSourceName() << " |";
502 static const char * const targets[] =
503 { CLIENT, SERVER, NULL };
507 static const char * const commands[] =
508 { COMMAND_ADD, COMMAND_REPLACE, COMMAND_DELETE, NULL };
509 for (int command = 0;
512 cout << right << setw(3) <<
513 ssr->getItemReportSuccessfulCount(targets[target], commands[command]);
515 cout << left << setw(3) <<
516 ssr->getItemReportCount(targets[target], commands[command]);
525 // compare databases?
526 if (!m_quiet && m_prepared) {
527 cout << "\nChanges applied to client during synchronization:\n";
528 for( iterator it = begin();
531 cout << "*** " << (*it)->getName() << " ***\n" << flush;
533 string before = databaseName(**it, "before");
534 string after = databaseName(**it, "after");
535 string cmd = string("synccompare '" ) +
536 before + "' '" + after +
537 "' && echo 'no changes'";
550 /** returns current sources as array as expected by SyncClient::sync(), memory owned by this class */
551 SyncSource **getSourceArray() {
552 m_sourceArray.reset(new SyncSource *[size() + 1]);
555 for (iterator it = begin();
558 m_sourceArray[index] = *it;
561 m_sourceArray[index] = 0;
562 return &m_sourceArray[0];
565 /** returns names of active sources */
566 set<string> getSources() {
569 BOOST_FOREACH(SyncSource *source, *this) {
570 res.insert(source->getName());
577 for( iterator it = begin();
584 /** find sync source by name */
585 EvolutionSyncSource *operator [] (const string &name) {
586 for (iterator it = begin();
589 if (name == (*it)->getName()) {
597 EvolutionSyncSource *operator [] (int index) { return vector<EvolutionSyncSource *>::operator [] (index); }
600 void unref(SourceList *sourceList)
605 string EvolutionSyncClient::askPassword(const string &descr)
609 printf("Enter password for %s: ",
612 if (fgets(buffer, sizeof(buffer), stdin) &&
613 strcmp(buffer, "\n")) {
614 size_t len = strlen(buffer);
615 if (len && buffer[len - 1] == '\n') {
620 throwError(string("could not read password for ") + descr);
625 void EvolutionSyncClient::throwError(const string &error)
629 * Catching the runtime_exception fails due to a toolchain problem,
630 * so do the error handling now and abort: because there is just
631 * one sync source this is probably the only thing that can be done.
632 * Still, it's not nice on the server...
634 fatalError(NULL, error.c_str());
636 throw runtime_error(error);
640 void EvolutionSyncClient::fatalError(void *object, const char *error)
642 LOG.error("%s", error);
643 if (m_sourceListPtr) {
644 m_sourceListPtr->syncDone(false);
650 * There have been segfaults inside glib in the background
651 * thread which ran the second event loop. Disabled it again,
652 * even though the synchronous EDS API calls will block then
655 #if 0 && defined(HAVE_GLIB) && defined(HAVE_EDS)
656 # define RUN_GLIB_LOOP
662 static void *mainLoopThread(void *)
664 // The test framework uses SIGALRM for timeouts.
665 // Block the signal here because a) the signal handler
666 // prints a stack back trace when called and we are not
667 // interessted in the background thread's stack and b)
668 // it seems to have confused glib/libebook enough to
669 // access invalid memory and segfault when it gets the SIGALRM.
671 sigemptyset(&blocked);
672 sigaddset(&blocked, SIGALRM);
673 pthread_sigmask(SIG_BLOCK, &blocked, NULL);
675 GMainLoop *mainloop = g_main_loop_new(NULL, TRUE);
677 g_main_loop_run(mainloop);
678 g_main_loop_unref(mainloop);
684 void EvolutionSyncClient::startLoopThread()
687 // when using Evolution we must have a running main loop,
688 // otherwise loss of connection won't be reported to us
689 static pthread_t loopthread;
690 static bool loopthreadrunning;
691 if (!loopthreadrunning) {
692 loopthreadrunning = !pthread_create(&loopthread, NULL, mainLoopThread, NULL);
697 AbstractSyncSourceConfig* EvolutionSyncClient::getAbstractSyncSourceConfig(const char* name) const
699 return m_sourceListPtr ? (*m_sourceListPtr)[name] : NULL;
702 AbstractSyncSourceConfig* EvolutionSyncClient::getAbstractSyncSourceConfig(unsigned int i) const
704 return m_sourceListPtr ? (*m_sourceListPtr)[i] : NULL;
707 unsigned int EvolutionSyncClient::getAbstractSyncSourceConfigsCount() const
709 return m_sourceListPtr ? m_sourceListPtr->size() : 0;
713 void EvolutionSyncClient::initSources(SourceList &sourceList)
715 set<string> unmatchedSources = m_sources;
716 list<string> configuredSources = getSyncSources();
717 for (list<string>::const_iterator it = configuredSources.begin();
718 it != configuredSources.end();
720 const string &name(*it);
721 boost::shared_ptr<PersistentEvolutionSyncSourceConfig> sc(getSyncSourceConfig(name));
723 // is the source enabled?
724 string sync = sc->getSync();
725 bool enabled = sync != "disabled";
726 bool overrideMode = false;
729 if (m_sources.size()) {
730 if (m_sources.find(sc->getName()) != m_sources.end()) {
735 unmatchedSources.erase(sc->getName());
742 string url = getSyncURL();
743 boost::replace_first(url, "https://", "http://"); // do not distinguish between protocol in change tracking
744 string changeId = string("sync4jevolution:") + url + "/" + name;
745 EvolutionSyncSourceParams params(name,
746 getSyncSourceNodes(name),
748 // the sync mode has to be set before instantiating the source
749 // because the client library reads the preferredSyncMode at that time:
750 // have to take a shortcut and set the property via its name
752 params.m_nodes.m_configNode->addFilter("sync", "two-way");
754 EvolutionSyncSource *syncSource =
755 EvolutionSyncSource::createSource(params);
757 throwError(name + ": type unknown" );
759 sourceList.push_back(syncSource);
763 // check whether there were any sources specified which do not exist
764 if (unmatchedSources.size()) {
767 for (set<string>::const_iterator it = unmatchedSources.begin();
768 it != unmatchedSources.end();
770 if (sources.size()) {
775 throwError(string("no such source(s): ") + sources);
779 int EvolutionSyncClient::sync()
784 LOG.error("No configuration for server \"%s\" found.", m_server.c_str());
785 throwError("cannot proceed without configuration");
788 // redirect logging as soon as possible
789 SourceList sourceList(m_server, m_doLogging, *this, m_quiet);
790 m_sourceListPtr = &sourceList;
793 sourceList.setLogdir(getLogDir(),
797 // dump some summary information at the beginning of the log
798 #ifdef LOG_HAVE_DEVELOPER
799 # define LOG_DEVELOPER developer
801 # define LOG_DEVELOPER debug
803 LOG.LOG_DEVELOPER("SyncML server account: %s", getUsername());
804 LOG.LOG_DEVELOPER("client: SyncEvolution %s for %s",
805 getSwv(), getDevType());
807 // instantiate backends, but do not open them yet
808 initSources(sourceList);
810 // request all config properties once: throwing exceptions
811 // now is okay, whereas later it would lead to leaks in the
812 // not exception safe client library
813 EvolutionSyncConfig dummy;
814 set<string> activeSources = sourceList.getSources();
815 dummy.copy(*this, &activeSources);
817 // start background thread if not running yet:
818 // necessary to catch problems with Evolution backend
821 // ask for passwords now
822 checkPassword(*this);
824 checkProxyPassword(*this);
826 BOOST_FOREACH(EvolutionSyncSource *source, sourceList) {
827 source->checkPassword(*this);
830 // open each source - failing now is still safe
831 BOOST_FOREACH(EvolutionSyncSource *source, sourceList) {
835 // give derived class also a chance to update the configs
836 prepare(sourceList.getSourceArray());
838 // ready to go: dump initial databases and prepare for final report
839 sourceList.syncPrepare();
842 res = SyncClient::sync(*this, sourceList.getSourceArray());
844 // store modified properties: must be done even after failed
845 // sync because the source's anchor might have been reset
849 if (getLastErrorCode() && getLastErrorMsg() && getLastErrorMsg()[0]) {
850 throwError(getLastErrorMsg());
852 // no error code/description?!
853 throwError("sync failed without an error description, check log");
856 // all went well: print final report before cleaning up
857 sourceList.syncDone(true);
860 } catch (const std::exception &ex) {
861 LOG.error( "%s", ex.what() );
863 // something went wrong, but try to write .after state anyway
864 m_sourceListPtr = NULL;
865 sourceList.syncDone(false);
867 LOG.error( "unknown error" );
868 m_sourceListPtr = NULL;
869 sourceList.syncDone(false);
872 m_sourceListPtr = NULL;
876 void EvolutionSyncClient::prepare(SyncSource **sources) {
877 if (m_syncMode != SYNC_NONE) {
878 for (SyncSource **source = sources;
881 (*source)->setPreferredSyncMode(m_syncMode);
886 void EvolutionSyncClient::status()
888 EvolutionSyncConfig config(m_server);
890 LOG.error("No configuration for server \"%s\" found.", m_server.c_str());
891 throwError("cannot proceed without configuration");
894 SourceList sourceList(m_server, false, *this, false);
895 initSources(sourceList);
896 BOOST_FOREACH(EvolutionSyncSource *source, sourceList) {
897 source->checkPassword(*this);
899 BOOST_FOREACH(EvolutionSyncSource *source, sourceList) {
903 sourceList.setLogdir(getLogDir(), 0, LOG_LEVEL_NONE);
904 LOG.setLevel(LOG_LEVEL_INFO);
905 string prevLogdir = sourceList.getPrevLogdir();
906 bool found = access(prevLogdir.c_str(), R_OK|X_OK) == 0;
910 sourceList.setPath(prevLogdir);
911 sourceList.dumpDatabases("current");
912 sourceList.dumpLocalChanges("after", "current");
913 } catch(const std::exception &ex) {
914 LOG.error("%s", ex.what());
917 cerr << "Previous log directory not found.\n";
918 if (!getLogDir() || !getLogDir()[0]) {
919 cerr << "Enable the 'logdir' option and synchronize to use this feature.\n";