common/FileUtil.cpp       |  119 ++++++++++++++++++++++++++++++----------------
 common/FileUtil.hpp       |   47 +++++++++++++++++-
 common/JailUtil.cpp       |   88 +++++++++++++++++++++++++---------
 loolwsd-systemplate-setup |   12 +---
 4 files changed, 195 insertions(+), 71 deletions(-)

New commits:
commit d6259d6a54e0e7873b2f4eb844c75bf0003245c6
Author:     Ashod Nakashian <ashod.nakash...@collabora.co.uk>
AuthorDate: Thu Jul 2 17:54:28 2020 -0400
Commit:     Ashod Nakashian <ashnak...@gmail.com>
CommitDate: Tue Jul 7 19:05:23 2020 +0200

    wsd: support parallel systemplate setup
    
    When tests are run in parallel, they will all
    compete to update and set up the systemplate
    directory, which has a handful of files that
    need to be up-to-date. This is a source of errors.
    
    Normally, these files are linked (hard- or soft-
    link, whichever succeeds). With linking, we
    only need to worry about the initial setup,
    as the files will never be out-of-date from
    then on. However, when linking fails, we need
    to copy the files, and update them (by copying
    over fresh versions of the files, if necessary)
    every time a new kit is forked. Copying over
    is tricky, as it's not atomic. To make it
    atomic, we copy the files to the destination
    directory under a temporary (random) name,
    and then rename to the final name (which is
    atomic, including replacing the target file,
    if it exists).
    
    No such race exists in production, where there
    is (or should be) but one instance of loolwsd
    (which does the initial setup) and forkit
    (which updates systemplate before forking
    new kit instances).
    This is an issue with parallel tests only.
    
    Change-Id: I6ba1514d00a84da7397d28efeb6378619711d52f
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/97785
    Tested-by: Jenkins
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>
    Reviewed-by: Ashod Nakashian <ashnak...@gmail.com>

diff --git a/common/FileUtil.cpp b/common/FileUtil.cpp
index 560a1b659..1f31e536d 100644
--- a/common/FileUtil.cpp
+++ b/common/FileUtil.cpp
@@ -13,6 +13,7 @@
 
 #include <dirent.h>
 #include <ftw.h>
+#include <sys/time.h>
 #ifdef __linux
 #include <sys/vfs.h>
 #elif defined IOS
@@ -85,7 +86,7 @@ namespace FileUtil
         return name;
     }
 
-    void copyFileTo(const std::string &fromPath, const std::string &toPath)
+    bool copy(const std::string& fromPath, const std::string& toPath, bool 
log, bool throw_on_error)
     {
         int from = -1, to = -1;
         try {
@@ -110,7 +111,10 @@ namespace FileUtil
                 throw;
             }
 
-            LOG_INF("Copying " << st.st_size << " bytes from " << 
anonymizeUrl(fromPath) << " to " << anonymizeUrl(toPath));
+            // Logging may be redundant and/or noisy.
+            if (log)
+                LOG_INF("Copying " << st.st_size << " bytes from " << 
anonymizeUrl(fromPath)
+                                   << " to " << anonymizeUrl(toPath));
 
             char buffer[64 * 1024];
 
@@ -150,15 +154,24 @@ namespace FileUtil
             }
             close(from);
             close(to);
+            return true;
         }
         catch (...)
         {
-            LOG_SYS("Failed to copy from " << anonymizeUrl(fromPath) << " to " 
<< anonymizeUrl(toPath));
+            std::ostringstream oss;
+            oss << "Failed to copy from " << anonymizeUrl(fromPath) << " to "
+                << anonymizeUrl(toPath);
+            const std::string err = oss.str();
+
+            LOG_SYS(err);
             close(from);
             close(to);
             unlink(toPath.c_str());
-            throw Poco::Exception("failed to copy");
+            if (throw_on_error)
+                throw Poco::Exception(err);
         }
+
+        return false;
     }
 
     std::string getTempFilePath(const std::string& srcDir, const std::string& 
srcFilename, const std::string& dstFilenamePrefix)
@@ -252,6 +265,69 @@ namespace FileUtil
         return path;
     }
 
+    bool isEmptyDirectory(const char* path)
+    {
+        DIR* dir = opendir(path);
+        if (dir == nullptr)
+            return errno != EACCES; // Assume it's not empty when EACCES.
+
+        int count = 0;
+        while (readdir(dir) && ++count < 3)
+            ;
+
+        closedir(dir);
+        return count <= 2; // Discounting . and ..
+    }
+
+    bool updateTimestamps(const std::string& filename, timespec tsAccess, 
timespec tsModified)
+    {
+        // The timestamp is in seconds and microseconds.
+        timeval timestamps[2]{ { tsAccess.tv_sec, tsAccess.tv_nsec / 1000 },
+                               { tsModified.tv_sec, tsModified.tv_nsec / 1000 
} };
+        if (utimes(filename.c_str(), timestamps) != 0)
+        {
+            LOG_SYS("Failed to update the timestamp of [" << filename << "]");
+            return false;
+        }
+
+        return true;
+    }
+
+    bool copyAtomic(const std::string& fromPath, const std::string& toPath, 
bool preserveTimestamps)
+    {
+        const std::string randFilename = toPath + Util::rng::getFilename(12);
+        if (copy(fromPath, randFilename, /*log=*/false, 
/*throw_on_error=*/false))
+        {
+            if (preserveTimestamps)
+            {
+                const Stat st(fromPath);
+                updateTimestamps(randFilename, st.sb().st_atim, 
st.sb().st_mtim);
+            }
+
+            // Now rename atomically, replacing any existing files with the 
same name.
+            if (rename(randFilename.c_str(), toPath.c_str()) == 0)
+                return true;
+
+            LOG_SYS("Failed to copy [" << fromPath << "] -> [" << toPath
+                                       << "] while atomically renaming:");
+            removeFile(randFilename, false); // Cleanup.
+        }
+
+        return false;
+    }
+
+    bool linkOrCopyFile(const char* source, const char* target)
+    {
+        if (link(source, target) == -1)
+        {
+            LOG_INF("link(\"" << source << "\", \"" << target << "\") failed: 
" << strerror(errno)
+                              << ". Will copy.");
+            return copy(source, target, /*log=*/false, 
/*throw_on_error=*/false);
+        }
+
+        return true;
+    }
+
 } // namespace FileUtil
 
 namespace
@@ -406,41 +482,6 @@ namespace FileUtil
         return AnonymizeUserData ? Util::anonymize(username, 
AnonymizationSalt) : username;
     }
 
-    bool isEmptyDirectory(const char* path)
-    {
-        DIR* dir = opendir(path);
-        if (dir == nullptr)
-            return errno != EACCES; // Assume it's not empty when EACCES.
-
-        int count = 0;
-        while (readdir(dir) && ++count < 3)
-            ;
-
-        closedir(dir);
-        return count <= 2; // Discounting . and ..
-    }
-
-    bool linkOrCopyFile(const char* source, const char* target)
-    {
-        if (link(source, target) == -1)
-        {
-            LOG_INF("link(\"" << source << "\", \"" << target << "\") failed: 
" << strerror(errno)
-                              << ". Will copy.");
-            try
-            {
-                Poco::File(source).copyTo(target);
-            }
-            catch (const std::exception& exc)
-            {
-                LOG_ERR("Copying of [" << source << "] to [" << target
-                                       << "] failed: " << exc.what());
-                return false;
-            }
-        }
-
-        return true;
-    }
-
 } // namespace FileUtil
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/common/FileUtil.hpp b/common/FileUtil.hpp
index 79342a46d..7d484d3d5 100644
--- a/common/FileUtil.hpp
+++ b/common/FileUtil.hpp
@@ -70,8 +70,25 @@ namespace FileUtil
     bool isEmptyDirectory(const char* path);
     inline bool isEmptyDirectory(const std::string& path) { return 
isEmptyDirectory(path.c_str()); }
 
+    /// Update the access-time and modified-time metadata for the given file.
+    bool updateTimestamps(const std::string& filename, timespec tsAccess, 
timespec tsModified);
+
+    /// Copy the source file to the target.
+    bool copy(const std::string& fromPath, const std::string& toPath, bool log,
+              bool throw_on_error);
+
+    /// Atomically copy a file and optionally preserve its timestamps.
+    /// The file is copied with a temporary name, and then atomically renamed.
+    /// NOTE: toPath must be a valid filename, not a directory.
+    /// Does not log (except errors), does not throw. Returns true on success.
+    bool copyAtomic(const std::string& fromPath, const std::string& toPath,
+                    bool preserveTimestamps);
+
     /// Copy a file from @fromPath to @toPath, throws on failure.
-    void copyFileTo(const std::string &fromPath, const std::string &toPath);
+    inline void copyFileTo(const std::string& fromPath, const std::string& 
toPath)
+    {
+        copy(fromPath, toPath, /*log=*/true, /*throw_on_error=*/true);
+    }
 
     /// Make a temp copy of a file, and prepend it with a prefix.
     std::string getTempFilePath(const std::string& srcDir, const std::string& 
srcFilename,
@@ -100,7 +117,7 @@ namespace FileUtil
     class Stat
     {
     public:
-        /// Stat the given path. Symbolic links are stats when @link is true.
+        /// Stat the given path. Symbolic links are stat'ed when @link is true.
         Stat(const std::string& file, bool link = false)
             : _path(file)
             , _res(link ? lstat(file.c_str(), &_sb) : stat(file.c_str(), &_sb))
@@ -119,9 +136,33 @@ namespace FileUtil
         bool isFile() const { return S_ISREG(_sb.st_mode); }
         bool isLink() const { return S_ISLNK(_sb.st_mode); }
 
-        /// Returns true iff the path exists, regarlesss of access permission.
+        /// Returns the filesize in bytes.
+        size_t size() const { return _sb.st_size; }
+
+        /// Returns the modified time.
+        timespec modifiedTime() const { return _sb.st_mtim; }
+
+        /// Returns true iff the path exists, regardless of access permission.
         bool exists() const { return good() || (_errno != ENOENT && _errno != 
ENOTDIR); }
 
+        /// Returns true if both files exist and have
+        /// the same size and modified timestamp.
+        bool isUpToDate(const Stat& other) const
+        {
+            if (exists() && other.exists() && !isDirectory() && 
!other.isDirectory())
+            {
+                // No need to check whether they are linked or not,
+                // since if they are, the following check will match,
+                // and if they aren't, we still need to rely on the following.
+                return (size() == other.size()
+                        && modifiedTime().tv_sec == other.modifiedTime().tv_sec
+                        && (modifiedTime().tv_nsec / 1000000) // Millisecond 
precision.
+                               == (other.modifiedTime().tv_nsec / 1000000));
+            }
+
+            return false;
+        }
+
     private:
         const std::string _path;
         struct ::stat _sb;
diff --git a/common/JailUtil.cpp b/common/JailUtil.cpp
index a104b91b1..bc0138c0f 100644
--- a/common/JailUtil.cpp
+++ b/common/JailUtil.cpp
@@ -245,63 +245,109 @@ namespace SysTemplate
 /// These must be up-to-date, as they can change during
 /// the long lifetime of our process. Also, it's unlikely
 /// that systemplate will get re-generated after installation.
-static const auto DynamicFilePaths = { "/etc/passwd", "/etc/group",         
"/etc/host.conf",
-                                       "/etc/hosts",  "/etc/nsswitch.conf", 
"/etc/resolv.conf" };
+static const auto DynamicFilePaths
+    = { "/etc/passwd",        "/etc/group",       "/etc/host.conf", 
"/etc/hosts",
+        "/etc/nsswitch.conf", "/etc/resolv.conf", "etc/timezone",   
"etc/localtime" };
 
-/// Copy by default for KIT_IN_PROCESS.
+/// Copy (false) by default for KIT_IN_PROCESS.
 static bool LinkDynamicFiles = false;
 
 void setupDynamicFiles(const std::string& sysTemplate)
 {
-    LOG_INF("Setting up dynamic files in sysTemplate.");
+    LOG_INF("Setting up systemplate dynamic files in [" << sysTemplate << 
"].");
 
     const std::string etcSysTemplatePath = Poco::Path(sysTemplate, 
"etc").toString();
-    LinkDynamicFiles = true;
+    LinkDynamicFiles = true; // Prefer linking, unless it fails.
     for (const auto& srcFilename : DynamicFilePaths)
     {
         const Poco::File srcFilePath(srcFilename);
-        if (!srcFilePath.exists())
+        FileUtil::Stat srcStat(srcFilename);
+        if (!srcStat.exists())
             continue;
 
-        // Remove the file to create a symlink.
-        const Poco::Path dstFilePath(sysTemplate, srcFilename);
+        const std::string dstFilename = Poco::Path(sysTemplate, 
srcFilename).toString();
+        FileUtil::Stat dstStat(dstFilename);
+
+        // Is it outdated?
+        if (dstStat.isUpToDate(srcStat))
+        {
+            LOG_INF("File [" << dstFilename << "] is already up-to-date.");
+            continue;
+        }
+
         if (LinkDynamicFiles)
         {
-            LOG_INF("Linking [" << srcFilename << "] -> [" << 
dstFilePath.toString() << "].");
-            FileUtil::removeFile(dstFilePath);
+            LOG_INF("Linking [" << srcFilename << "] -> [" << dstFilename << 
"].");
 
             // Link or copy.
-            if (link(srcFilename, dstFilePath.toString().c_str()) != -1)
+            if (link(srcFilename, dstFilename.c_str()) == 0)
+                continue;
+
+            // Hard-linking failed, try symbolic linking.
+            if (symlink(srcFilename, dstFilename.c_str()) == 0)
+                continue;
+
+            const int linkerr = errno;
+
+            // With parallel tests, another test might have linked already.
+            if (Poco::File(dstFilename).exists()) // stat again.
                 continue;
 
             // Failed to link a file. Disable linking and copy instead.
-            LOG_WRN("Failed to link [" << srcFilename << "] -> [" << 
dstFilePath.toString() << "] ("
-                                       << strerror(errno) << "). Will copy.");
+            LOG_WRN("Failed to link ["
+                    << srcFilename << "] -> [" << dstFilename << "] (" << 
strerror(linkerr)
+                    << "). Will copy and disable linking dynamic system files 
in this run.");
             LinkDynamicFiles = false;
         }
 
-        // Linking fails, just copy.
-        LOG_INF("Copying [" << srcFilename << "] -> [" << 
dstFilePath.toString() << "].");
-        srcFilePath.copyTo(etcSysTemplatePath);
+        // Linking failed, just copy.
+        LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilename << "].");
+        if (!FileUtil::copyAtomic(srcFilename, dstFilename, true))
+        {
+            if (!Poco::File(dstFilename).exists()) // stat again.
+                LOG_ERR("Failed to copy [" << srcFilename << "] -> [" << 
dstFilename
+                                           << "], some functionality may be 
missing.");
+        }
     }
+
+    if (LinkDynamicFiles)
+        LOG_INF("Successfully linked the systemplate dynamic files in ["
+                << sysTemplate << "] and will not need to update them again.");
 }
 
 void updateDynamicFiles(const std::string& sysTemplate)
 {
+    // If the files are linked, they are always up-to-date.
     if (!LinkDynamicFiles)
     {
-        LOG_INF("Updating dynamic files in sysTemplate.");
+        LOG_INF("Updating systemplate dynamic files in [" << sysTemplate << 
"].");
 
         const std::string etcSysTemplatePath = Poco::Path(sysTemplate, 
"etc").toString();
         for (const auto& srcFilename : DynamicFilePaths)
         {
             const Poco::File srcFilePath(srcFilename);
-            if (!srcFilePath.exists())
+            FileUtil::Stat srcStat(srcFilename);
+            if (!srcStat.exists())
                 continue;
 
-            const Poco::Path dstFilePath(sysTemplate, srcFilename);
-            LOG_DBG("Copying [" << srcFilename << "] -> [" << 
dstFilePath.toString() << "].");
-            srcFilePath.copyTo(etcSysTemplatePath);
+            const std::string dstFilename = Poco::Path(sysTemplate, 
srcFilename).toString();
+            FileUtil::Stat dstStat(dstFilename);
+
+            // Is it outdated?
+            if (dstStat.isUpToDate(srcStat))
+            {
+                LOG_INF("File [" << dstFilename << "] is already up-to-date.");
+            }
+            else
+            {
+                LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilename 
<< "].");
+                if (!FileUtil::copyAtomic(srcFilename, dstFilename, true))
+                {
+                    if (!Poco::File(dstFilename).exists()) // stat again.
+                        LOG_ERR("Failed to copy [" << srcFilename << "] -> [" 
<< dstFilename
+                                                   << "], some functionality 
may be missing.");
+                }
+            }
         }
     }
 }
diff --git a/loolwsd-systemplate-setup b/loolwsd-systemplate-setup
index 5b7bda6e8..2f6e462e0 100755
--- a/loolwsd-systemplate-setup
+++ b/loolwsd-systemplate-setup
@@ -21,16 +21,12 @@ cd / || exit 1
 # into the template tree of system files for the chroot jails.
 
 # First essential files and shared objects
-find etc/hosts etc/nsswitch.conf etc/resolv.conf \
-     etc/passwd etc/group etc/host.conf \
-     etc/ld.so.* \
+find etc/ld.so.* \
      lib/ld-* lib64/ld-* \
      lib/libnss_* lib64/libnss_* lib/*/libnss_* \
      lib/libresolv* lib64/libresolv* lib/*/libresolv* \
      var/cache/fontconfig \
      etc/fonts \
-     etc/timezone \
-     etc/localtime \
      usr/lib/locale/en_US.utf8 \
      usr/lib/locale/C.UTF-8 \
      usr/lib/locale/locale_archive \
@@ -42,9 +38,6 @@ find etc/hosts etc/nsswitch.conf etc/resolv.conf \
         -type f 2>/dev/null
 
 find etc/fonts \
-     etc/timezone \
-     etc/localtime \
-     etc/resolv.conf \
      lib/ld-* lib64/ld-* \
      lib/libnss_* lib64/libnss_* lib/*/libnss_* \
      lib/libresolv* lib64/libresolv* lib/*/libresolv* \
@@ -65,6 +58,9 @@ grep -v dynamic | cut -d " " -f 3 | grep -E '^(/lib|/usr)' | 
sort -u | sed -e 's
 # This will now copy the file a symlink points to, but whatever.
 cpio -p -d -L $CHROOT
 
+# Remove the dynamic files, which are linked by loolwsd.
+rm -f 
$CHROOT/etc/{hosts,nsswitch.conf,resolv.conf,passwd,group,host.conf,timezone,localtime}
+
 mkdir -p $CHROOT/lo
 mkdir -p $CHROOT/dev
 mkdir -p $CHROOT/tmp/dev
_______________________________________________
Libreoffice-commits mailing list
libreoffice-comm...@lists.freedesktop.org
https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits

Reply via email to