Skip to content

Commit ca2ab0a

Browse files
committed
fix: replace the lockfile package in order to depend on modified, instead of created when checking the stale locks
Replacing the lock package, we are able to keep smaller `stale` values (e.g. 10 seconds) and avoid losing the lock during long-running operations. In this way, when the CLI process crashes (or is killed with SIGKILL), we won't have leaked lock files for more than 10 seconds.
1 parent 97246f8 commit ca2ab0a

File tree

12 files changed

+1026
-767
lines changed

12 files changed

+1026
-767
lines changed

Gruntfile.js

+1-8
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@ var travis = process.env["TRAVIS"];
2929
var buildNumber = process.env["PACKAGE_VERSION"] || process.env["BUILD_NUMBER"] || "non-ci";
3030

3131
module.exports = function (grunt) {
32-
var path = require("path");
33-
var commonLibNodeModules = path.join("lib", "common", "node_modules");
34-
if (require("fs").existsSync(commonLibNodeModules)) {
35-
grunt.file.delete(commonLibNodeModules);
36-
}
37-
grunt.file.write(path.join("lib", "common", ".d.ts"), "");
38-
3932
grunt.initConfig({
4033
copyPackageTo: process.env["CopyPackageTo"] || ".",
4134

@@ -229,7 +222,7 @@ module.exports = function (grunt) {
229222
if (travis && process.env.TRAVIS_PULL_REQUEST_BRANCH) {
230223
return grunt.task.run("pack");
231224
}
232-
225+
233226
// Set correct version in Travis job, so the deploy will not publish strict version (for example 5.2.0).
234227
grunt.task.run("set_package_version");
235228
console.log(`Skipping pack step as the current build is not from PR, so it will be packed from the deploy provider.`);

lib/common/Gruntfile.js

-179
This file was deleted.

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

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as net from "net";
22
import { performanceLog } from "../../decorators";
3-
import { APPLICATION_RESPONSE_TIMEOUT_SECONDS } from "../../../constants";
43

54
export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
65
private cachedSockets: IDictionary<net.Socket> = {};
@@ -35,12 +34,7 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
3534

3635
return this.cachedSockets[appId];
3736
},
38-
`ios-debug-socket-${this.deviceInfo.identifier}-${appId}.lock`,
39-
{
40-
// increase the timeout with `APPLICATION_RESPONSE_TIMEOUT_SECONDS` as a workaround
41-
// till startApplication is resolved before the application is really started
42-
stale: (APPLICATION_RESPONSE_TIMEOUT_SECONDS + 30) * 1000,
43-
}
37+
`ios-debug-socket-${this.deviceInfo.identifier}-${appId}.lock`
4438
);
4539
}
4640

lib/common/services/livesync-service-base.ts

-4
This file was deleted.

lib/common/services/lock-service.ts

+50-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import * as lockfile from "lockfile";
1+
import * as lockfile from "proper-lockfile";
22
import * as path from "path";
33
import { cache } from "../decorators";
4+
import { getHash } from "../helpers";
45

56
export class LockService implements ILockService {
67
private currentlyLockedFiles: string[] = [];
@@ -15,12 +16,11 @@ export class LockService implements ILockService {
1516
}
1617

1718
private get defaultLockParams(): ILockOptions {
18-
// 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.
19-
// In case lock is older than the `stale` value, consider it stale and try to get a new lock.
2019
const lockParams: ILockOptions = {
21-
retryWait: 100,
22-
retries: 100,
23-
stale: 30 * 1000,
20+
// https://www.npmjs.com/package/retry#retrytimeoutsoptions
21+
retriesObj: { retries: 13, minTimeout: 100, maxTimeout: 1000, factor: 2 },
22+
stale: 10 * 1000,
23+
realpath: false
2424
};
2525

2626
return lockParams;
@@ -31,41 +31,49 @@ export class LockService implements ILockService {
3131
private $processService: IProcessService) {
3232
this.$processService.attachToProcessExitSignals(this, () => {
3333
const locksToRemove = _.clone(this.currentlyLockedFiles);
34-
_.each(locksToRemove, lock => {
35-
this.unlock(lock);
36-
});
34+
for (const lockToRemove of locksToRemove) {
35+
lockfile.unlockSync(lockToRemove);
36+
this.cleanLock(lockToRemove);
37+
}
3738
});
3839
}
3940

4041
public async executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockOpts?: ILockOptions): Promise<T> {
41-
const resolvedLockFilePath = await this.lock(lockFilePath, lockOpts);
42+
const releaseFunc = await this.lock(lockFilePath, lockOpts);
4243

4344
try {
4445
const result = await action();
4546
return result;
4647
} finally {
47-
this.unlock(resolvedLockFilePath);
48+
releaseFunc();
4849
}
4950
}
5051

51-
public lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string> {
52+
public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<() => void> {
5253
const { filePath, fileOpts } = this.getLockFileSettings(lockFilePath, lockOpts);
5354
this.currentlyLockedFiles.push(filePath);
55+
this.$fs.writeFile(filePath, "");
5456

55-
// Prevent ENOENT error when the dir, where lock should be created, does not exist.
56-
this.$fs.ensureDirectoryExists(path.dirname(filePath));
57-
58-
return new Promise<string>((resolve, reject) => {
59-
lockfile.lock(filePath, fileOpts, (err: Error) => {
60-
err ? reject(new Error(`Timeout while waiting for lock "${filePath}"`)) : resolve(filePath);
61-
});
62-
});
57+
try {
58+
const releaseFunc = await lockfile.lock(filePath, fileOpts);
59+
return async () => {
60+
await releaseFunc();
61+
this.cleanLock(filePath);
62+
};
63+
} catch (err) {
64+
throw new Error(`Timeout while waiting for lock "${filePath}"`);
65+
}
6366
}
6467

6568
public unlock(lockFilePath?: string): void {
6669
const { filePath } = this.getLockFileSettings(lockFilePath);
67-
_.remove(this.currentlyLockedFiles, e => e === lockFilePath);
6870
lockfile.unlockSync(filePath);
71+
this.cleanLock(filePath);
72+
}
73+
74+
private cleanLock(lockPath: string): void {
75+
_.remove(this.currentlyLockedFiles, e => e === lockPath);
76+
this.$fs.deleteFile(lockPath);
6977
}
7078

7179
private getLockFileSettings(filePath?: string, fileOpts?: ILockOptions): { filePath: string, fileOpts: ILockOptions } {
@@ -76,11 +84,31 @@ export class LockService implements ILockService {
7684
filePath = filePath || this.defaultLockFilePath;
7785
fileOpts = fileOpts ? _.assign({}, this.defaultLockParams, fileOpts) : this.defaultLockParams;
7886

87+
fileOpts.retriesObj = fileOpts.retriesObj || {};
88+
if (fileOpts.retries) {
89+
fileOpts.retriesObj.retries = fileOpts.retries;
90+
}
91+
92+
if (fileOpts.retryWait) {
93+
// backwards compatibility
94+
fileOpts.retriesObj.minTimeout = fileOpts.retriesObj.maxTimeout = fileOpts.retryWait;
95+
}
96+
97+
(<any>fileOpts.retries) = fileOpts.retriesObj;
98+
7999
return {
80-
filePath,
100+
filePath: this.getShortFileLock(filePath),
81101
fileOpts
82102
};
83103
}
104+
105+
private getShortFileLock(filePath: string) {
106+
const dirPath = path.dirname(filePath);
107+
const fileName = path.basename(filePath);
108+
const hashedFileName = getHash(fileName, { algorithm: "MD5" });
109+
filePath = path.join(dirPath, hashedFileName);
110+
return filePath;
111+
}
84112
}
85113

86114
$injector.register("lockService", LockService);

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as util from "util";
44
import { EventEmitter } from "events";
55

66
export class LockServiceStub implements ILockService {
7-
public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string> {
8-
return lockFilePath;
7+
public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<() => void> {
8+
return () => { };
99
}
1010

1111
public unlock(lockFilePath?: string): void {

lib/common/tsconfig.json

-18
This file was deleted.

0 commit comments

Comments
 (0)