Skip to content

Commit dd50fa2

Browse files
author
Molly Howell
committed
Bug 1553982 Part 1 - Manage "multi-instance locks" that can be created by any component. r=bytesized,nalexander
Differential Revision: https://phabricator.services.mozilla.com/D95626
1 parent 814e884 commit dd50fa2

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed

toolkit/xre/MultiInstanceLock.cpp

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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/. */
6+
7+
#include "MultiInstanceLock.h"
8+
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
14+
15+
#ifndef XP_WIN
16+
# include <fcntl.h>
17+
# include <sys/stat.h>
18+
# include <sys/types.h>
19+
#endif
20+
21+
namespace mozilla {
22+
23+
static bool GetLockFileName(const char* nameToken, const char16_t* installPath,
24+
nsCString& filePath) {
25+
mozilla::UniquePtr<NS_tchar[]> pathHash;
26+
if (!GetInstallHash(installPath, MOZ_APP_VENDOR, pathHash)) {
27+
return false;
28+
}
29+
30+
#ifdef XP_WIN
31+
// On Windows, the lock file is placed at the path
32+
// ProgramData\[vendor]\[nameToken]-[pathHash], so first we need to get the
33+
// ProgramData path and then append our directory and the file name.
34+
PWSTR programDataPath;
35+
HRESULT hr = SHGetKnownFolderPath(FOLDERID_ProgramData, KF_FLAG_CREATE,
36+
nullptr, &programDataPath);
37+
if (FAILED(hr)) {
38+
return false;
39+
}
40+
mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> programDataPathUnique(
41+
programDataPath);
42+
43+
filePath = nsPrintfCString("%S\\%s\\%s-%S", programDataPath, MOZ_APP_VENDOR,
44+
nameToken, pathHash.get());
45+
46+
#else
47+
// On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash].
48+
filePath = nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR, nameToken,
49+
pathHash.get());
50+
51+
#endif
52+
53+
return true;
54+
}
55+
56+
MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken,
57+
const char16_t* installPath) {
58+
nsCString filePath;
59+
GetLockFileName(nameToken, installPath, filePath);
60+
61+
// Open a file handle with full privileges and sharing, and then attempt to
62+
// take a shared (nonexclusive, read-only) lock on it.
63+
#ifdef XP_WIN
64+
HANDLE h =
65+
::CreateFileW(PromiseFlatString(NS_ConvertUTF8toUTF16(filePath)).get(),
66+
GENERIC_READ | GENERIC_WRITE,
67+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
68+
nullptr, OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr);
69+
if (h != INVALID_HANDLE_VALUE) {
70+
// The LockFileEx functions always require an OVERLAPPED structure even
71+
// though we did not open the lock file for overlapped I/O.
72+
OVERLAPPED o = {0};
73+
if (!::LockFileEx(h, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
74+
CloseHandle(h);
75+
h = INVALID_HANDLE_VALUE;
76+
}
77+
}
78+
return h;
79+
80+
#else
81+
int fd = ::open(PromiseFlatCString(filePath).get(),
82+
O_CLOEXEC | O_CREAT | O_NOFOLLOW,
83+
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
84+
if (fd != -1) {
85+
// We would like to ensure that the lock file is deleted when we are done
86+
// with it. The normal way to do that would be to call unlink on it right
87+
// now, but that would immediately delete the name from the file system, and
88+
// we need other instances to be able to open that name and get the same
89+
// inode, so we can't unlink the file before we're done with it. This means
90+
// we accept some unreliability in getting the file deleted, but it's a zero
91+
// byte file in the tmp directory, so having it stay around isn't the worst.
92+
struct flock l = {0};
93+
l.l_start = 0;
94+
l.l_len = 0;
95+
l.l_type = F_RDLCK;
96+
if (::fcntl(fd, F_SETLK, &l)) {
97+
::close(fd);
98+
fd = -1;
99+
}
100+
}
101+
return fd;
102+
103+
#endif
104+
}
105+
106+
void ReleaseMultiInstanceLock(MultiInstLockHandle lock) {
107+
if (lock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
108+
#ifdef XP_WIN
109+
OVERLAPPED o = {0};
110+
::UnlockFileEx(lock, 0, 1, 0, &o);
111+
// We've used FILE_FLAG_DELETE_ON_CLOSE, so if we are the last instance
112+
// with a handle on the lock file, closing it here will delete it.
113+
::CloseHandle(lock);
114+
115+
#else
116+
// If we're the last instance, then unlink the lock file. There is a race
117+
// condition here that may cause an instance to fail to open the same inode
118+
// as another even though they use the same path, but there's no reasonable
119+
// way to avoid that without skipping deleting the file at all, so we accept
120+
// that risk.
121+
bool otherInstance = true;
122+
if (IsOtherInstanceRunning(lock, &otherInstance) && !otherInstance) {
123+
// Recover the file's path so we can unlink it.
124+
// There's no error checking in here because we're content to let the file
125+
// hang around if any of this fails (which can happen if for example we're
126+
// on a system where /proc/self/fd does not exist); this is a zero-byte
127+
// file in the tmp directory after all.
128+
UniquePtr<NS_tchar[]> linkPath = MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
129+
NS_tsnprintf(linkPath.get(), MAXPATHLEN + 1, "/proc/self/fd/%d", lock);
130+
UniquePtr<NS_tchar[]> lockFilePath =
131+
MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
132+
if (::readlink(linkPath.get(), lockFilePath.get(), MAXPATHLEN + 1) !=
133+
-1) {
134+
::unlink(lockFilePath.get());
135+
}
136+
}
137+
// Now close the lock file, which will release the lock.
138+
::close(lock);
139+
#endif
140+
}
141+
}
142+
143+
bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult) {
144+
// Every running instance has opened a readonly lock, and read locks prevent
145+
// write locks from being opened, so to see if we are the only instance, we
146+
// attempt to take a write lock, and if it succeeds then that must mean there
147+
// are no other read locks open and therefore no other instances.
148+
if (lock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
149+
return false;
150+
}
151+
152+
#ifdef XP_WIN
153+
// We need to release the lock we're holding before we would be allowed to
154+
// take an exclusive lock, and if that succeeds we need to release it too
155+
// in order to get our shared lock back. This procedure is not atomic, so we
156+
// accept the risk of the scheduler deciding to ruin our day between these
157+
// operations; we'd get a false negative in a different instance's check.
158+
OVERLAPPED o = {0};
159+
// Release our current shared lock.
160+
if (!::UnlockFileEx(lock, 0, 1, 0, &o)) {
161+
return false;
162+
}
163+
// Attempt to take an exclusive lock.
164+
bool rv = false;
165+
if (::LockFileEx(lock, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0,
166+
1, 0, &o)) {
167+
// We got the exclusive lock, so now release it.
168+
::UnlockFileEx(lock, 0, 1, 0, &o);
169+
*aResult = false;
170+
rv = true;
171+
} else if (::GetLastError() == ERROR_LOCK_VIOLATION) {
172+
// We didn't get the exclusive lock because of outstanding shared locks.
173+
*aResult = true;
174+
rv = true;
175+
}
176+
// Attempt to reclaim the shared lock we released at the beginning.
177+
if (!::LockFileEx(lock, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
178+
rv = false;
179+
}
180+
return rv;
181+
182+
#else
183+
// See if we would be allowed to set a write lock (no need to actually do so).
184+
struct flock l = {0};
185+
l.l_start = 0;
186+
l.l_len = 0;
187+
l.l_type = F_WRLCK;
188+
if (::fcntl(lock, F_GETLK, &l)) {
189+
return false;
190+
}
191+
*aResult = l.l_type != F_UNLCK;
192+
return true;
193+
194+
#endif
195+
}
196+
197+
}; // namespace mozilla

toolkit/xre/MultiInstanceLock.h

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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/. */
6+
7+
#ifndef MULTIINSTANCELOCK_H
8+
#define MULTIINSTANCELOCK_H
9+
10+
#ifdef XP_WIN
11+
# include <windows.h>
12+
#endif
13+
14+
// These functions manage "multi-instance locks", which are a type of lock
15+
// specifically designed to allow instances of an application, process, or other
16+
// task to detect when other instances relevant to them are running. Each
17+
// instance opens a lock and holds it for the duration of the task of interest
18+
// (which may be the lifetime of the process, or a shorter period). Then while
19+
// the lock is open, it can be used to check whether any other instances of the
20+
// same task are currently running out of the same copy of the binary, in the
21+
// context of any OS user. A process can open any number of locks, so long as
22+
// they use different names. It is necessary for the process to have permission
23+
// to create files in /tmp/ on POSIX systems or ProgramData\[vendor]\ on
24+
// Windows, so this mechanism may not work for sandboxed processes.
25+
26+
// The implementation is based on file locking. An empty file is created in a
27+
// systemwide (not per-user) location, and a shared (read) lock is taken on that
28+
// file; the value that OpenMultiInstanceLock() returns is the file
29+
// handle/descriptor. When you call IsOtherInstanceRunning(), it will attempt to
30+
// convert that shared lock into an exclusive (write) lock. If that operation
31+
// would succeed, it means that there must not be any other shared locks
32+
// currently taken on that file, so we know there are no other instances
33+
// running. This is a more complex design than most file locks or most other
34+
// concurrency mechanisms, but it is necessary for this use case because of the
35+
// requirement that an instance must be able to detect other instances that were
36+
// started later than it was. If, say, a mutex were used, or another kind of
37+
// exclusive lock, then the first instance that tried to take it would succeed,
38+
// and be unable to tell that another instance had tried to take it later and
39+
// failed. This mechanism allows any number of instances started at any time in
40+
// relation to one another to always be able to detect that the others exist
41+
// (although it does not allow you to know how many others exist). The lock is
42+
// guaranteed to be released if the process holding it crashes or is exec'd into
43+
// something else, because the file is closed when that happens. The file itself
44+
// is not necessarily always deleted on POSIX, because it isn't possible (within
45+
// reason) to guarantee that unlink() is called, but the file is empty and
46+
// created in the /tmp directory, so should not be a serious problem.
47+
48+
namespace mozilla {
49+
50+
#ifdef XP_WIN
51+
using MultiInstLockHandle = HANDLE;
52+
# define MULTI_INSTANCE_LOCK_HANDLE_ERROR INVALID_HANDLE_VALUE
53+
#else
54+
using MultiInstLockHandle = int;
55+
# define MULTI_INSTANCE_LOCK_HANDLE_ERROR -1
56+
#endif
57+
58+
/*
59+
* nameToken should be a string very briefly naming the lock you are creating
60+
* creating, and it should be unique except for across multiple instances of the
61+
* same application. The vendor name is included in the generated path, so it
62+
* doesn't need to be present in your supplied name. Try to keep this name sort
63+
* of short, ideally under about 64 characters, because creating the lock will
64+
* fail if the final path string (the token + the path hash + the vendor name)
65+
* is longer than the platform's maximum path and/or path component length.
66+
*
67+
* installPath should be the path to the directory containing the application,
68+
* which will be used to form a path specific to that installation.
69+
*
70+
* Returns MULTI_INSTANCE_LOCK_HANDLE_ERROR upon failure, or a handle which can
71+
* later be passed to the other functions declared here upon success.
72+
*/
73+
MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken,
74+
const char16_t* installPath);
75+
76+
void ReleaseMultiInstanceLock(MultiInstLockHandle lock);
77+
78+
// aResult will be set to true if another instance *was* found, false if not.
79+
// Return value is true on success, false on error (and aResult won't be set).
80+
bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult);
81+
82+
}; // namespace mozilla
83+
84+
#endif // MULTIINSTANCELOCK_H

toolkit/xre/moz.build

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ EXPORTS.mozilla += [
3838
"AutoSQLiteLifetime.h",
3939
"Bootstrap.h",
4040
"CmdLineAndEnvUtils.h",
41+
"MultiInstanceLock.h",
4142
"SafeMode.h",
4243
"UntrustedModulesData.h",
4344
]
@@ -125,6 +126,11 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
125126
"nsAndroidStartup.cpp",
126127
]
127128

129+
if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android":
130+
UNIFIED_SOURCES += [
131+
"MultiInstanceLock.cpp",
132+
]
133+
128134
UNIFIED_SOURCES += [
129135
"/toolkit/mozapps/update/common/commonupdatedir.cpp",
130136
"AutoSQLiteLifetime.cpp",

0 commit comments

Comments
 (0)