Skip to content

Commit d12300e

Browse files
committed
fix: lock the debug connection execution in order to avoid possible issues with multiple device socket connections and attach requests, remove all locks on process exit
1 parent 7445a9d commit d12300e

File tree

9 files changed

+89
-98
lines changed

9 files changed

+89
-98
lines changed

lib/common/declarations.d.ts

-61
Original file line numberDiff line numberDiff line change
@@ -1958,67 +1958,6 @@ interface IiOSNotificationService {
19581958
postNotification(deviceIdentifier: string, notification: string, commandType?: string): Promise<number>;
19591959
}
19601960

1961-
/**
1962-
* Copied from @types/lockfile
1963-
* Describes the options that can be passed to lockfile methods.
1964-
*/
1965-
interface ILockFileOptions {
1966-
/**
1967-
* A number of milliseconds to wait for locks to expire before giving up.
1968-
* Only used by lockFile.lock. Poll for opts.wait ms.
1969-
* If the lock is not cleared by the time the wait expires, then it returns with the original error.
1970-
*/
1971-
wait?: number;
1972-
1973-
/**
1974-
* When using opts.wait, this is the period in ms in which it polls to check if the lock has expired. Defaults to 100.
1975-
*/
1976-
pollPeriod?: number;
1977-
1978-
/**
1979-
* A number of milliseconds before locks are considered to have expired.
1980-
*/
1981-
stale?: number;
1982-
1983-
/**
1984-
* Used by lock and lockSync. Retry n number of times before giving up.
1985-
*/
1986-
retries?: number;
1987-
1988-
/**
1989-
* Used by lock. Wait n milliseconds before retrying.
1990-
*/
1991-
retryWait?: number;
1992-
}
1993-
1994-
/**
1995-
* Describes methods that can be used to use file locking.
1996-
*/
1997-
interface ILockFile {
1998-
/**
1999-
* Acquire a file lock on the specified path.
2000-
* @param {string} lockFilePath Path to lockfile that has to be created. Defaults to `<profile dir>/lockfile.lock`
2001-
* @param {ILockFileOptions} lockFileOpts Options used for creating the lockfile.
2002-
* @returns {Promise<void>}
2003-
*/
2004-
lock(lockFilePath?: string, lockFileOpts?: ILockFileOptions): Promise<void>;
2005-
2006-
/**
2007-
* Close and unlink the lockfile.
2008-
* @param {string} lockFilePath Path to lockfile that has to be created. Defaults to `<profile dir>/lockfile.lock`
2009-
* @returns {void}
2010-
*/
2011-
unlock(lockFilePath?: string): void;
2012-
2013-
/**
2014-
* Check if the lockfile is locked and not stale.
2015-
* @param {string} lockFilePath Path to lockfile that has to be created. Defaults to `<profile dir>/lockfile.lock`
2016-
* @param {ILockFileOptions} lockFileOpts Options used for creating the lockfile.
2017-
* @returns {boolean} true in case file is locked, false otherwise
2018-
*/
2019-
check(lockFilePath?: string, lockFileOpts?: ILockFileOptions): boolean;
2020-
}
2021-
20221961
declare module "stringify-package" {
20231962
function stringifyPackage(data: any, indent: any, newline: string): string
20241963
export = stringifyPackage

lib/common/mobile/ios/device/ios-device.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class IOSDevice extends IOSDeviceBase {
1717
protected $errors: IErrors,
1818
private $injector: IInjector,
1919
protected $iOSDebuggerPortService: IIOSDebuggerPortService,
20+
protected $lockfile: ILockFile,
2021
private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor,
2122
protected $processService: IProcessService,
2223
private $deviceLogProvider: Mobile.IDeviceLogProvider,
@@ -63,6 +64,7 @@ export class IOSDevice extends IOSDeviceBase {
6364
await this.$iOSSocketRequestExecutor.executeAttachRequest(this, constants.AWAIT_NOTIFICATION_TIMEOUT_SECONDS, appId);
6465
const port = await this.getDebuggerPort(appId);
6566
const deviceId = this.deviceInfo.identifier;
67+
6668
const socket = await helpers.connectEventuallyUntilTimeout(
6769
async () => {
6870
const deviceResponse = _.first((await this.$iosDeviceOperations.connectToPort([{ deviceId: deviceId, port: port }]))[deviceId]);

lib/common/mobile/ios/ios-device-base.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
55
protected abstract $errors: IErrors;
66
protected abstract $iOSDebuggerPortService: IIOSDebuggerPortService;
77
protected abstract $processService: IProcessService;
8+
protected abstract $lockfile: ILockFile;
89
abstract deviceInfo: Mobile.IDeviceInfo;
910
abstract applicationManager: Mobile.IDeviceApplicationManager;
1011
abstract fileSystem: Mobile.IDeviceFileSystem;
@@ -24,21 +25,23 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
2425
}
2526

2627
public async getSocket(appId: string): Promise<net.Socket> {
27-
if (this.cachedSockets[appId]) {
28-
return this.cachedSockets[appId];
29-
}
28+
return this.$lockfile.executeActionWithLock(async () => {
29+
if (this.cachedSockets[appId]) {
30+
return this.cachedSockets[appId];
31+
}
3032

31-
this.cachedSockets[appId] = await this.getSocketCore(appId);
33+
this.cachedSockets[appId] = await this.getSocketCore(appId);
3234

33-
if (this.cachedSockets[appId]) {
34-
this.cachedSockets[appId].on("close", () => {
35-
this.destroySocket(appId);
36-
});
35+
if (this.cachedSockets[appId]) {
36+
this.cachedSockets[appId].on("close", () => {
37+
this.destroySocket(appId);
38+
});
3739

38-
this.$processService.attachToProcessExitSignals(this, () => this.destroySocket(appId));
39-
}
40+
this.$processService.attachToProcessExitSignals(this, () => this.destroySocket(appId));
41+
}
4042

41-
return this.cachedSockets[appId];
43+
return this.cachedSockets[appId];
44+
}, "ios-debug-socket.lock");
4245
}
4346

4447
public destroyLiveSyncSocket(appId: string) {

lib/common/mobile/ios/simulator/ios-simulator-device.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class IOSSimulator extends IOSDeviceBase implements Mobile.IiOSDevice {
1414
constructor(private simulator: Mobile.IiSimDevice,
1515
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1616
protected $errors: IErrors,
17+
protected $lockfile: ILockFile,
1718
private $injector: IInjector,
1819
protected $iOSDebuggerPortService: IIOSDebuggerPortService,
1920
private $iOSSimResolver: Mobile.IiOSSimResolver,

lib/common/services/lockfile.ts

+40-11
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,82 @@ import * as path from "path";
33
import { cache } from "../decorators";
44

55
export class LockFile implements ILockFile {
6+
private currentlyLockedFiles: string[] = [];
67

78
@cache()
89
private get defaultLockFilePath(): string {
9-
return path.join(this.$settingsService.getProfileDir(), "lockfile.lock");
10+
return this.getAbsoluteLockFilePath("lockfile.lock");
1011
}
1112

12-
private get defaultLockParams(): lockfile.Options {
13+
private getAbsoluteLockFilePath(relativeLockFilePath: string) {
14+
return path.join(this.$settingsService.getProfileDir(), relativeLockFilePath);
15+
}
16+
17+
private get defaultLockParams(): ILockFileOptions {
1318
// We'll retry 100 times and time between retries is 100ms, i.e. full wait in case we are unable to get lock will be 10 seconds.
14-
// In case lock is older than 3 minutes, consider it stale and try to get a new lock.
15-
const lockParams: lockfile.Options = {
19+
// In case lock is older than the `stale` value, consider it stale and try to get a new lock.
20+
const lockParams: ILockFileOptions = {
1621
retryWait: 100,
1722
retries: 100,
18-
stale: 180 * 1000,
23+
stale: 30 * 1000,
1924
};
2025

2126
return lockParams;
2227
}
2328

2429
constructor(private $fs: IFileSystem,
25-
private $settingsService: ISettingsService) {
30+
private $settingsService: ISettingsService,
31+
private $processService: IProcessService) {
32+
this.$processService.attachToProcessExitSignals(this, () => {
33+
const locksToRemove = _.clone(this.currentlyLockedFiles);
34+
_.each(locksToRemove, lock => {
35+
this.unlock(lock);
36+
});
37+
});
2638
}
2739

28-
public lock(lockFilePath?: string, lockFileOpts?: lockfile.Options): Promise<void> {
40+
public lock(lockFilePath?: string, lockFileOpts?: ILockFileOptions): Promise<string> {
2941
const { filePath, fileOpts } = this.getLockFileSettings(lockFilePath, lockFileOpts);
42+
this.currentlyLockedFiles.push(filePath);
3043

3144
// Prevent ENOENT error when the dir, where lock should be created, does not exist.
3245
this.$fs.ensureDirectoryExists(path.dirname(filePath));
3346

34-
return new Promise<void>((resolve, reject) => {
47+
return new Promise<string>((resolve, reject) => {
3548
lockfile.lock(filePath, fileOpts, (err: Error) => {
36-
err ? reject(err) : resolve();
49+
err ? reject(new Error(`Timeout while waiting for lock "${filePath}"`)) : resolve(filePath);
3750
});
3851
});
3952
}
4053

54+
public async executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockFileOpts?: ILockFileOptions): Promise<T> {
55+
const resolvedLockFilePath = await this.lock(lockFilePath, lockFileOpts);
56+
57+
try {
58+
const result = await action();
59+
return result;
60+
} finally {
61+
this.unlock(resolvedLockFilePath);
62+
}
63+
}
64+
4165
public unlock(lockFilePath?: string): void {
4266
const { filePath } = this.getLockFileSettings(lockFilePath);
67+
_.remove(this.currentlyLockedFiles, e => e === lockFilePath);
4368
lockfile.unlockSync(filePath);
4469
}
4570

46-
public check(lockFilePath?: string, lockFileOpts?: lockfile.Options): boolean {
71+
public check(lockFilePath?: string, lockFileOpts?: ILockFileOptions): boolean {
4772
const { filePath, fileOpts } = this.getLockFileSettings(lockFilePath, lockFileOpts);
4873

4974
return lockfile.checkSync(filePath, fileOpts);
5075
}
5176

52-
private getLockFileSettings(filePath?: string, fileOpts?: lockfile.Options): { filePath: string, fileOpts: lockfile.Options } {
77+
private getLockFileSettings(filePath?: string, fileOpts?: ILockFileOptions): { filePath: string, fileOpts: ILockFileOptions } {
78+
if (filePath && !path.isAbsolute(filePath)) {
79+
filePath = this.getAbsoluteLockFilePath(filePath);
80+
}
81+
5382
filePath = filePath || this.defaultLockFilePath;
5483
fileOpts = fileOpts || this.defaultLockParams;
5584

lib/common/services/user-settings-service.ts

+4-14
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class UserSettingsServiceBase implements IUserSettingsService {
55
private userSettingsFilePath: string = null;
66
protected userSettingsData: any = null;
77
private get lockFilePath(): string {
8-
return `${this.userSettingsFilePath}.lock`;
8+
return `user-settings.lock`;
99
}
1010

1111
constructor(userSettingsFilePath: string,
@@ -21,7 +21,7 @@ export class UserSettingsServiceBase implements IUserSettingsService {
2121
return this.userSettingsData ? this.userSettingsData[settingName] : null;
2222
};
2323

24-
return this.executeActionWithLock<T>(action);
24+
return this.$lockfile.executeActionWithLock<T>(action, this.lockFilePath);
2525
}
2626

2727
public async saveSetting<T>(key: string, value: T): Promise<void> {
@@ -39,17 +39,7 @@ export class UserSettingsServiceBase implements IUserSettingsService {
3939
await this.saveSettings();
4040
};
4141

42-
return this.executeActionWithLock<void>(action);
43-
}
44-
45-
private async executeActionWithLock<T>(action: () => Promise<T>): Promise<T> {
46-
try {
47-
await this.$lockfile.lock(this.lockFilePath);
48-
const result = await action();
49-
return result;
50-
} finally {
51-
this.$lockfile.unlock(this.lockFilePath);
52-
}
42+
return this.$lockfile.executeActionWithLock<void>(action, this.lockFilePath);
5343
}
5444

5545
public saveSettings(data?: any): Promise<void> {
@@ -66,7 +56,7 @@ export class UserSettingsServiceBase implements IUserSettingsService {
6656
this.$fs.writeJson(this.userSettingsFilePath, this.userSettingsData);
6757
};
6858

69-
return this.executeActionWithLock<void>(action);
59+
return this.$lockfile.executeActionWithLock<void>(action, this.lockFilePath);
7060
}
7161

7262
// TODO: Remove Promise, reason: writeFile - blocked as other implementation of the interface has async operation.

lib/common/test/unit-tests/mobile/ios-simulator-discovery.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Yok } from "../../../yok";
44
import { assert } from "chai";
55
import { DeviceDiscoveryEventNames, CONNECTED_STATUS } from "../../../constants";
66
import { DevicePlatformsConstants } from "../../../mobile/device-platforms-constants";
7-
import { ErrorsStub, CommonLoggerStub, HooksServiceStub } from "../stubs";
7+
import { ErrorsStub, CommonLoggerStub, HooksServiceStub, LockFileStub } from "../stubs";
88
import { FileSystemStub } from "../../../../../test/stubs";
99

1010
let currentlyRunningSimulators: Mobile.IiSimDevice[];
@@ -16,6 +16,7 @@ function createTestInjector(): IInjector {
1616
injector.register("injector", injector);
1717
injector.register("errors", ErrorsStub);
1818
injector.register("iOSDebuggerPortService", {});
19+
injector.register("lockfile", LockFileStub);
1920
injector.register("iOSSimResolver", {
2021
iOSSim: {
2122
getRunningSimulators: async () => currentlyRunningSimulators

lib/common/test/unit-tests/stubs.ts

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
import * as util from "util";
44
import { EventEmitter } from "events";
55

6+
export class LockFileStub implements ILockFile {
7+
public async executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockFileOpts?: ILockFileOptions): Promise<T> {
8+
const result = await action();
9+
return result;
10+
}
11+
}
12+
613
export class CommonLoggerStub implements ILogger {
714
setLevel(level: string): void { }
815
getLevel(): string { return undefined; }

lib/definitions/lockfile.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as lockfile from "lockfile";
2+
3+
declare global {
4+
interface ILockFileOptions extends lockfile.Options { }
5+
6+
/**
7+
* Describes methods that can be used to use file locking.
8+
*/
9+
interface ILockFile {
10+
/**
11+
* @param action The code to be locked.
12+
* @param {string} lockFilePath Path to lockfile that has to be created. Defaults to `<profile dir>/lockfile.lock`
13+
* @param {ILockFileOptions} lockFileOpts Options used for creating the lockfile.
14+
* @returns {Promise<T>}
15+
*/
16+
executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockFileOpts?: ILockFileOptions): Promise<T>
17+
// TODO: expose as decorator
18+
}
19+
}

0 commit comments

Comments
 (0)