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
19 # include <sys/stat.h>
20 # include <sys/types.h>
24 # include "WinUtils.h"
27 #ifdef MOZ_WIDGET_COCOA
28 # include "nsILocalFileMac.h"
33 static bool GetLockFileName(const char* nameToken
, const char16_t
* installPath
,
34 nsCString
& filePath
) {
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
);
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());
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
65 if (hashPtr
== updateDir
.get()) {
69 // We need to make a copy of the hash before we modify updateDir to get the
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
);
78 // Get the root update dir from the update dir.
79 BOOL success
= PathRemoveFileSpecW(updateDir
.get());
83 success
= PathRemoveFileSpecW(updateDir
.get());
89 nsPrintfCString("%s\\%s-%s", NS_ConvertUTF16toUTF8(updateDir
.get()).get(),
90 nameToken
, NS_ConvertUTF16toUTF8(hash
.get()).get());
93 mozilla::UniquePtr
<NS_tchar
[]> pathHash
;
94 if (!GetInstallHash(installPath
, pathHash
)) {
98 // On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash].
99 filePath
= nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR
, nameToken
,
107 MultiInstLockHandle
OpenMultiInstanceLock(const char* nameToken
,
108 const char16_t
* installPath
) {
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.
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.
126 if (!::LockFileEx(h
, LOCKFILE_FAIL_IMMEDIATELY
, 0, 1, 0, &o
)) {
128 h
= INVALID_HANDLE_VALUE
;
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
);
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};
149 if (::fcntl(fd
, F_SETLK
, &l
)) {
159 void ReleaseMultiInstanceLock(MultiInstLockHandle lock
) {
160 if (lock
!= MULTI_INSTANCE_LOCK_HANDLE_ERROR
) {
163 ::UnlockFileEx(lock
, 0, 1, 0, &o
);
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
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) !=
185 ::unlink(lockFilePath
.get());
188 // Now close the lock file, which will release the lock.
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
) {
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.
210 // Release our current shared lock.
211 if (!::UnlockFileEx(lock
, 0, 1, 0, &o
)) {
214 // Attempt to take an exclusive lock.
216 if (::LockFileEx(lock
, LOCKFILE_EXCLUSIVE_LOCK
| LOCKFILE_FAIL_IMMEDIATELY
, 0,
218 // We got the exclusive lock, so now release it.
219 ::UnlockFileEx(lock
, 0, 1, 0, &o
);
222 } else if (::GetLastError() == ERROR_LOCK_VIOLATION
) {
223 // We didn't get the exclusive lock because of outstanding shared locks.
227 // Attempt to reclaim the shared lock we released at the beginning.
228 if (!::LockFileEx(lock
, LOCKFILE_FAIL_IMMEDIATELY
, 0, 1, 0, &o
)) {
234 // See if we would be allowed to set a write lock (no need to actually do so).
235 struct flock l
= {0};
239 if (::fcntl(lock
, F_GETLK
, &l
)) {
242 *aResult
= l
.l_type
!= F_UNLCK
;
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.
252 nsCOMPtr
<nsIFile
> appFile
;
254 rv
= aAppFile
->Clone(getter_AddRefs(appFile
));
255 NS_ENSURE_SUCCESS(rv
, nullptr);
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
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.
282 nsCOMPtr
<nsILocalFileMac
> macFile
= do_QueryInterface(appFile
);
283 if (macFile
&& NS_SUCCEEDED(macFile
->GetFSRef(&ref
)) &&
285 NS_NewLocalFileWithFSRef(&ref
, true, getter_AddRefs(macFile
)))) {
286 appFile
= static_cast<nsIFile
*>(macFile
);
288 NS_WARNING("Failed to resolve install directory.");
292 return appFile
.forget();
295 }; // namespace mozilla