Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / toolkit / xre / MultiInstanceLock.cpp
blob230df80a1f45f539f97c0a2ef6a39ccb857f8c69
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "MultiInstanceLock.h"
9 #include "commonupdatedir.h" // for GetInstallHash
10 #include "mozilla/UniquePtr.h"
11 #include "nsPrintfCString.h"
12 #include "nsPromiseFlatString.h"
13 #include "updatedefines.h" // for NS_t* definitions
15 #ifdef XP_WIN
16 # include <shlwapi.h>
17 #else
18 # include <fcntl.h>
19 # include <sys/stat.h>
20 # include <sys/types.h>
21 #endif
23 #ifdef XP_WIN
24 # include "WinUtils.h"
25 #endif
27 #ifdef MOZ_WIDGET_COCOA
28 # include "nsILocalFileMac.h"
29 #endif
31 namespace mozilla {
33 static bool GetLockFileName(const char* nameToken, const char16_t* installPath,
34 nsCString& filePath) {
35 #ifdef XP_WIN
36 // On Windows, the lock file is placed at the path
37 // [updateDirectory]\[nameToken]-[pathHash], so first we need to get the
38 // update directory path and then append the file name.
40 // Note: This will return something like
41 // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates\<hash>
42 // But we actually are going to want to return the root update directory,
43 // the grandparent of this directory, which will look something like this:
44 // C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38
45 mozilla::UniquePtr<wchar_t[]> updateDir;
46 HRESULT hr = GetCommonUpdateDirectory(
47 reinterpret_cast<const wchar_t*>(installPath), updateDir);
48 if (FAILED(hr)) {
49 return false;
52 // For the path manipulation that we are about to do, it is important that
53 // the update directory have no trailing slash.
54 size_t len = wcslen(updateDir.get());
55 if (len == 0) {
56 return false;
58 if (updateDir.get()[len - 1] == '/' || updateDir.get()[len - 1] == '\\') {
59 updateDir.get()[len - 1] = '\0';
62 wchar_t* hashPtr = PathFindFileNameW(updateDir.get());
63 // PathFindFileNameW returns a pointer to the beginning of the string on
64 // failure.
65 if (hashPtr == updateDir.get()) {
66 return false;
69 // We need to make a copy of the hash before we modify updateDir to get the
70 // root update dir.
71 size_t hashSize = wcslen(hashPtr) + 1;
72 mozilla::UniquePtr<wchar_t[]> hash = mozilla::MakeUnique<wchar_t[]>(hashSize);
73 errno_t error = wcscpy_s(hash.get(), hashSize, hashPtr);
74 if (error != 0) {
75 return false;
78 // Get the root update dir from the update dir.
79 BOOL success = PathRemoveFileSpecW(updateDir.get());
80 if (!success) {
81 return false;
83 success = PathRemoveFileSpecW(updateDir.get());
84 if (!success) {
85 return false;
88 filePath =
89 nsPrintfCString("%s\\%s-%s", NS_ConvertUTF16toUTF8(updateDir.get()).get(),
90 nameToken, NS_ConvertUTF16toUTF8(hash.get()).get());
92 #else
93 mozilla::UniquePtr<NS_tchar[]> pathHash;
94 if (!GetInstallHash(installPath, pathHash)) {
95 return false;
98 // On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash].
99 filePath = nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR, nameToken,
100 pathHash.get());
102 #endif
104 return true;
107 MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken,
108 const char16_t* installPath) {
109 nsCString filePath;
110 if (!GetLockFileName(nameToken, installPath, filePath)) {
111 return MULTI_INSTANCE_LOCK_HANDLE_ERROR;
114 // Open a file handle with full privileges and sharing, and then attempt to
115 // take a shared (nonexclusive, read-only) lock on it.
116 #ifdef XP_WIN
117 HANDLE h =
118 ::CreateFileW(PromiseFlatString(NS_ConvertUTF8toUTF16(filePath)).get(),
119 GENERIC_READ | GENERIC_WRITE,
120 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
121 nullptr, OPEN_ALWAYS, 0, nullptr);
122 if (h != INVALID_HANDLE_VALUE) {
123 // The LockFileEx functions always require an OVERLAPPED structure even
124 // though we did not open the lock file for overlapped I/O.
125 OVERLAPPED o = {0};
126 if (!::LockFileEx(h, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
127 CloseHandle(h);
128 h = INVALID_HANDLE_VALUE;
131 return h;
133 #else
134 int fd = ::open(PromiseFlatCString(filePath).get(),
135 O_CLOEXEC | O_CREAT | O_NOFOLLOW,
136 S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
137 if (fd != -1) {
138 // We would like to ensure that the lock file is deleted when we are done
139 // with it. The normal way to do that would be to call unlink on it right
140 // now, but that would immediately delete the name from the file system, and
141 // we need other instances to be able to open that name and get the same
142 // inode, so we can't unlink the file before we're done with it. This means
143 // we accept some unreliability in getting the file deleted, but it's a zero
144 // byte file in the tmp directory, so having it stay around isn't the worst.
145 struct flock l = {0};
146 l.l_start = 0;
147 l.l_len = 0;
148 l.l_type = F_RDLCK;
149 if (::fcntl(fd, F_SETLK, &l)) {
150 ::close(fd);
151 fd = -1;
154 return fd;
156 #endif
159 void ReleaseMultiInstanceLock(MultiInstLockHandle lock) {
160 if (lock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
161 #ifdef XP_WIN
162 OVERLAPPED o = {0};
163 ::UnlockFileEx(lock, 0, 1, 0, &o);
164 ::CloseHandle(lock);
166 #else
167 // If we're the last instance, then unlink the lock file. There is a race
168 // condition here that may cause an instance to fail to open the same inode
169 // as another even though they use the same path, but there's no reasonable
170 // way to avoid that without skipping deleting the file at all, so we accept
171 // that risk.
172 bool otherInstance = true;
173 if (IsOtherInstanceRunning(lock, &otherInstance) && !otherInstance) {
174 // Recover the file's path so we can unlink it.
175 // There's no error checking in here because we're content to let the file
176 // hang around if any of this fails (which can happen if for example we're
177 // on a system where /proc/self/fd does not exist); this is a zero-byte
178 // file in the tmp directory after all.
179 UniquePtr<NS_tchar[]> linkPath = MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
180 NS_tsnprintf(linkPath.get(), MAXPATHLEN + 1, "/proc/self/fd/%d", lock);
181 UniquePtr<NS_tchar[]> lockFilePath =
182 MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
183 if (::readlink(linkPath.get(), lockFilePath.get(), MAXPATHLEN + 1) !=
184 -1) {
185 ::unlink(lockFilePath.get());
188 // Now close the lock file, which will release the lock.
189 ::close(lock);
190 #endif
194 bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult) {
195 // Every running instance has opened a readonly lock, and read locks prevent
196 // write locks from being opened, so to see if we are the only instance, we
197 // attempt to take a write lock, and if it succeeds then that must mean there
198 // are no other read locks open and therefore no other instances.
199 if (lock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
200 return false;
203 #ifdef XP_WIN
204 // We need to release the lock we're holding before we would be allowed to
205 // take an exclusive lock, and if that succeeds we need to release it too
206 // in order to get our shared lock back. This procedure is not atomic, so we
207 // accept the risk of the scheduler deciding to ruin our day between these
208 // operations; we'd get a false negative in a different instance's check.
209 OVERLAPPED o = {0};
210 // Release our current shared lock.
211 if (!::UnlockFileEx(lock, 0, 1, 0, &o)) {
212 return false;
214 // Attempt to take an exclusive lock.
215 bool rv = false;
216 if (::LockFileEx(lock, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0,
217 1, 0, &o)) {
218 // We got the exclusive lock, so now release it.
219 ::UnlockFileEx(lock, 0, 1, 0, &o);
220 *aResult = false;
221 rv = true;
222 } else if (::GetLastError() == ERROR_LOCK_VIOLATION) {
223 // We didn't get the exclusive lock because of outstanding shared locks.
224 *aResult = true;
225 rv = true;
227 // Attempt to reclaim the shared lock we released at the beginning.
228 if (!::LockFileEx(lock, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
229 rv = false;
231 return rv;
233 #else
234 // See if we would be allowed to set a write lock (no need to actually do so).
235 struct flock l = {0};
236 l.l_start = 0;
237 l.l_len = 0;
238 l.l_type = F_WRLCK;
239 if (::fcntl(lock, F_GETLK, &l)) {
240 return false;
242 *aResult = l.l_type != F_UNLCK;
243 return true;
245 #endif
248 already_AddRefed<nsIFile> GetNormalizedAppFile(nsIFile* aAppFile) {
249 // If we're given an app file, use it; otherwise, get it from the ambient
250 // directory service.
251 nsresult rv;
252 nsCOMPtr<nsIFile> appFile;
253 if (aAppFile) {
254 rv = aAppFile->Clone(getter_AddRefs(appFile));
255 NS_ENSURE_SUCCESS(rv, nullptr);
256 } else {
257 nsCOMPtr<nsIProperties> dirSvc =
258 do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
259 NS_ENSURE_TRUE(dirSvc, nullptr);
261 rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
262 getter_AddRefs(appFile));
263 NS_ENSURE_SUCCESS(rv, nullptr);
266 // It is possible that the path we have is on a case insensitive
267 // filesystem in which case the path may vary depending on how the
268 // application is called. We want to normalize the case somehow.
269 // On Linux XRE_EXECUTABLE_FILE already seems to be set to the correct path.
271 // See similar nsXREDirProvider::GetInstallHash. The main difference here is
272 // to allow lookup to fail on OSX, because some tests use a nonexistent
273 // appFile.
274 #ifdef XP_WIN
275 // Windows provides a way to get the correct case.
276 if (!mozilla::widget::WinUtils::ResolveJunctionPointsAndSymLinks(appFile)) {
277 NS_WARNING("Failed to resolve install directory.");
279 #elif defined(MOZ_WIDGET_COCOA)
280 // On OSX roundtripping through an FSRef fixes the case.
281 FSRef ref;
282 nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(appFile);
283 if (macFile && NS_SUCCEEDED(macFile->GetFSRef(&ref)) &&
284 NS_SUCCEEDED(
285 NS_NewLocalFileWithFSRef(&ref, true, getter_AddRefs(macFile)))) {
286 appFile = static_cast<nsIFile*>(macFile);
287 } else {
288 NS_WARNING("Failed to resolve install directory.");
290 #endif
292 return appFile.forget();
295 }; // namespace mozilla