summaryrefslogtreecommitdiffstats
path: root/toolkit/xre/MultiInstanceLock.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/xre/MultiInstanceLock.cpp
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/xre/MultiInstanceLock.cpp')
-rw-r--r--toolkit/xre/MultiInstanceLock.cpp297
1 files changed, 297 insertions, 0 deletions
diff --git a/toolkit/xre/MultiInstanceLock.cpp b/toolkit/xre/MultiInstanceLock.cpp
new file mode 100644
index 0000000000..eb4db6367a
--- /dev/null
+++ b/toolkit/xre/MultiInstanceLock.cpp
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MultiInstanceLock.h"
+
+#include "commonupdatedir.h" // for GetInstallHash
+#include "mozilla/UniquePtr.h"
+#include "nsPrintfCString.h"
+#include "nsPromiseFlatString.h"
+#include "nsXULAppAPI.h"
+#include "updatedefines.h" // for NS_t* definitions
+
+#ifdef XP_WIN
+# include <shlwapi.h>
+#else
+# include <fcntl.h>
+# include <sys/stat.h>
+# include <sys/types.h>
+#endif
+
+#ifdef XP_WIN
+# include "WinUtils.h"
+#endif
+
+#ifdef MOZ_WIDGET_COCOA
+# include "nsILocalFileMac.h"
+#endif
+
+namespace mozilla {
+
+bool GetMultiInstanceLockFileName(const char* nameToken,
+ const char16_t* installPath,
+ nsCString& filePath) {
+#ifdef XP_WIN
+ // On Windows, the lock file is placed at the path
+ // [updateDirectory]\[nameToken]-[pathHash], so first we need to get the
+ // update directory path and then append the file name.
+
+ // Note: This will return something like
+ // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates\<hash>
+ // But we actually are going to want to return the root update directory,
+ // the grandparent of this directory, which will look something like this:
+ // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38
+ mozilla::UniquePtr<wchar_t[]> updateDir;
+ HRESULT hr = GetCommonUpdateDirectory(
+ reinterpret_cast<const wchar_t*>(installPath), updateDir);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ // For the path manipulation that we are about to do, it is important that
+ // the update directory have no trailing slash.
+ size_t len = wcslen(updateDir.get());
+ if (len == 0) {
+ return false;
+ }
+ if (updateDir.get()[len - 1] == '/' || updateDir.get()[len - 1] == '\\') {
+ updateDir.get()[len - 1] = '\0';
+ }
+
+ wchar_t* hashPtr = PathFindFileNameW(updateDir.get());
+ // PathFindFileNameW returns a pointer to the beginning of the string on
+ // failure.
+ if (hashPtr == updateDir.get()) {
+ return false;
+ }
+
+ // We need to make a copy of the hash before we modify updateDir to get the
+ // root update dir.
+ size_t hashSize = wcslen(hashPtr) + 1;
+ mozilla::UniquePtr<wchar_t[]> hash = mozilla::MakeUnique<wchar_t[]>(hashSize);
+ errno_t error = wcscpy_s(hash.get(), hashSize, hashPtr);
+ if (error != 0) {
+ return false;
+ }
+
+ // Get the root update dir from the update dir.
+ BOOL success = PathRemoveFileSpecW(updateDir.get());
+ if (!success) {
+ return false;
+ }
+ success = PathRemoveFileSpecW(updateDir.get());
+ if (!success) {
+ return false;
+ }
+
+ filePath =
+ nsPrintfCString("%s\\%s-%s", NS_ConvertUTF16toUTF8(updateDir.get()).get(),
+ nameToken, NS_ConvertUTF16toUTF8(hash.get()).get());
+
+#else
+ mozilla::UniquePtr<NS_tchar[]> pathHash;
+ if (!GetInstallHash(installPath, pathHash)) {
+ return false;
+ }
+
+ // On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash].
+ filePath = nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR, nameToken,
+ pathHash.get());
+
+#endif
+
+ return true;
+}
+
+MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken,
+ const char16_t* installPath) {
+ nsCString filePath;
+ if (!GetMultiInstanceLockFileName(nameToken, installPath, filePath)) {
+ return MULTI_INSTANCE_LOCK_HANDLE_ERROR;
+ }
+
+ // Open a file handle with full privileges and sharing, and then attempt to
+ // take a shared (nonexclusive, read-only) lock on it.
+#ifdef XP_WIN
+ HANDLE h =
+ ::CreateFileW(PromiseFlatString(NS_ConvertUTF8toUTF16(filePath)).get(),
+ GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr, OPEN_ALWAYS, 0, nullptr);
+ if (h != INVALID_HANDLE_VALUE) {
+ // The LockFileEx functions always require an OVERLAPPED structure even
+ // though we did not open the lock file for overlapped I/O.
+ OVERLAPPED o = {0};
+ if (!::LockFileEx(h, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
+ CloseHandle(h);
+ h = INVALID_HANDLE_VALUE;
+ }
+ }
+ return h;
+
+#else
+ int fd = ::open(PromiseFlatCString(filePath).get(),
+ O_CLOEXEC | O_CREAT | O_NOFOLLOW,
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+ if (fd != -1) {
+ // We would like to ensure that the lock file is deleted when we are done
+ // with it. The normal way to do that would be to call unlink on it right
+ // now, but that would immediately delete the name from the file system, and
+ // we need other instances to be able to open that name and get the same
+ // inode, so we can't unlink the file before we're done with it. This means
+ // we accept some unreliability in getting the file deleted, but it's a zero
+ // byte file in the tmp directory, so having it stay around isn't the worst.
+ struct flock l = {0};
+ l.l_start = 0;
+ l.l_len = 0;
+ l.l_type = F_RDLCK;
+ if (::fcntl(fd, F_SETLK, &l)) {
+ ::close(fd);
+ fd = -1;
+ }
+ }
+ return fd;
+
+#endif
+}
+
+void ReleaseMultiInstanceLock(MultiInstLockHandle lock) {
+ if (lock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
+#ifdef XP_WIN
+ OVERLAPPED o = {0};
+ ::UnlockFileEx(lock, 0, 1, 0, &o);
+ ::CloseHandle(lock);
+
+#else
+ // If we're the last instance, then unlink the lock file. There is a race
+ // condition here that may cause an instance to fail to open the same inode
+ // as another even though they use the same path, but there's no reasonable
+ // way to avoid that without skipping deleting the file at all, so we accept
+ // that risk.
+ bool otherInstance = true;
+ if (IsOtherInstanceRunning(lock, &otherInstance) && !otherInstance) {
+ // Recover the file's path so we can unlink it.
+ // There's no error checking in here because we're content to let the file
+ // hang around if any of this fails (which can happen if for example we're
+ // on a system where /proc/self/fd does not exist); this is a zero-byte
+ // file in the tmp directory after all.
+ UniquePtr<NS_tchar[]> linkPath = MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
+ NS_tsnprintf(linkPath.get(), MAXPATHLEN + 1, "/proc/self/fd/%d", lock);
+ UniquePtr<NS_tchar[]> lockFilePath =
+ MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
+ if (::readlink(linkPath.get(), lockFilePath.get(), MAXPATHLEN + 1) !=
+ -1) {
+ ::unlink(lockFilePath.get());
+ }
+ }
+ // Now close the lock file, which will release the lock.
+ ::close(lock);
+#endif
+ }
+}
+
+bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult) {
+ // Every running instance has opened a readonly lock, and read locks prevent
+ // write locks from being opened, so to see if we are the only instance, we
+ // attempt to take a write lock, and if it succeeds then that must mean there
+ // are no other read locks open and therefore no other instances.
+ if (lock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
+ return false;
+ }
+
+#ifdef XP_WIN
+ // We need to release the lock we're holding before we would be allowed to
+ // take an exclusive lock, and if that succeeds we need to release it too
+ // in order to get our shared lock back. This procedure is not atomic, so we
+ // accept the risk of the scheduler deciding to ruin our day between these
+ // operations; we'd get a false negative in a different instance's check.
+ OVERLAPPED o = {0};
+ // Release our current shared lock.
+ if (!::UnlockFileEx(lock, 0, 1, 0, &o)) {
+ return false;
+ }
+ // Attempt to take an exclusive lock.
+ bool rv = false;
+ if (::LockFileEx(lock, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0,
+ 1, 0, &o)) {
+ // We got the exclusive lock, so now release it.
+ ::UnlockFileEx(lock, 0, 1, 0, &o);
+ *aResult = false;
+ rv = true;
+ } else if (::GetLastError() == ERROR_LOCK_VIOLATION) {
+ // We didn't get the exclusive lock because of outstanding shared locks.
+ *aResult = true;
+ rv = true;
+ }
+ // Attempt to reclaim the shared lock we released at the beginning.
+ if (!::LockFileEx(lock, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
+ rv = false;
+ }
+ return rv;
+
+#else
+ // See if we would be allowed to set a write lock (no need to actually do so).
+ struct flock l = {0};
+ l.l_start = 0;
+ l.l_len = 0;
+ l.l_type = F_WRLCK;
+ if (::fcntl(lock, F_GETLK, &l)) {
+ return false;
+ }
+ *aResult = l.l_type != F_UNLCK;
+ return true;
+
+#endif
+}
+
+already_AddRefed<nsIFile> GetNormalizedAppFile(nsIFile* aAppFile) {
+ // If we're given an app file, use it; otherwise, get it from the ambient
+ // directory service.
+ nsresult rv;
+ nsCOMPtr<nsIFile> appFile;
+ if (aAppFile) {
+ rv = aAppFile->Clone(getter_AddRefs(appFile));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ } else {
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(dirSvc, nullptr);
+
+ rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
+ getter_AddRefs(appFile));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+
+ // It is possible that the path we have is on a case insensitive
+ // filesystem in which case the path may vary depending on how the
+ // application is called. We want to normalize the case somehow.
+ // On Linux XRE_EXECUTABLE_FILE already seems to be set to the correct path.
+ //
+ // See similar nsXREDirProvider::GetInstallHash. The main difference here is
+ // to allow lookup to fail on OSX, because some tests use a nonexistent
+ // appFile.
+#ifdef XP_WIN
+ // Windows provides a way to get the correct case.
+ if (!mozilla::widget::WinUtils::ResolveJunctionPointsAndSymLinks(appFile)) {
+ NS_WARNING("Failed to resolve install directory.");
+ }
+#elif defined(MOZ_WIDGET_COCOA)
+ // On OSX roundtripping through an FSRef fixes the case.
+ FSRef ref;
+ nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(appFile);
+ if (macFile && NS_SUCCEEDED(macFile->GetFSRef(&ref)) &&
+ NS_SUCCEEDED(
+ NS_NewLocalFileWithFSRef(&ref, true, getter_AddRefs(macFile)))) {
+ appFile = static_cast<nsIFile*>(macFile);
+ } else {
+ NS_WARNING("Failed to resolve install directory.");
+ }
+#endif
+
+ return appFile.forget();
+}
+
+}; // namespace mozilla