Skip to content

Commit

Permalink
Retry in the find function for fs.realpathSync method for UNC-paths (#…
Browse files Browse the repository at this point in the history
…777)

Sometimes there are spontaneous issues when working with unc-paths, so retries have been added for them.
  • Loading branch information
DenisRumyantsev authored Jul 20, 2021
1 parent f192965 commit ce5aefa
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 5 deletions.
10 changes: 10 additions & 0 deletions node/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,16 @@ export function _loadData(): void {
// Internal path helpers.
//--------------------------------------------------------------------------------

/**
* Defines if path is unc-path.
*
* @param path a path to a file.
* @returns true if path starts with double backslash, otherwise returns false.
*/
export function _isUncPath(path: string) {
return /^\\\\[^\\]/.test(path);
}

export function _ensureRooted(root: string, p: string) {
if (!root) {
throw new Error('ensureRooted() parameter "root" cannot be empty');
Expand Down
4 changes: 4 additions & 0 deletions node/mock-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ export function cp(source: string, dest: string): void {
module.exports.debug('copying ' + source + ' to ' + dest);
}

export function retry(func: Function, args: any[], retryOptions: task.RetryOptions): any {
module.exports.debug(`trying to execute ${func?.name}(${args.toString()}) with ${retryOptions.retryCount} retries`);
}

export function find(findPath: string): string[] {
return mock.getResponse('find', findPath, module.exports.debug);
}
Expand Down
2 changes: 1 addition & 1 deletion node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azure-pipelines-task-lib",
"version": "3.1.4",
"version": "3.1.5",
"description": "Azure Pipelines Task SDK",
"main": "./task.js",
"typings": "./task.d.ts",
Expand Down
54 changes: 53 additions & 1 deletion node/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,52 @@ export interface FindOptions {
followSymbolicLinks: boolean;
}

/**
* Interface for RetryOptions
*
* Contains "continueOnError" and "retryCount" options.
*/
export interface RetryOptions {

/**
* If true, code still continues to execute when all retries failed.
*/
continueOnError: boolean,

/**
* Number of retries.
*/
retryCount: number
}

/**
* Tries to execute a function a specified number of times.
*
* @param func a function to be executed.
* @param args executed function arguments array.
* @param retryOptions optional. Defaults to { continueOnError: false, retryCount: 0 }.
* @returns the same as the usual function.
*/
export function retry(func: Function, args: any[], retryOptions: RetryOptions = { continueOnError: false, retryCount: 0 }): any {
while (retryOptions.retryCount >= 0) {
try {
return func(...args);
} catch (e) {
if (retryOptions.retryCount <= 0) {
if (retryOptions.continueOnError) {
warning(e);
break;
} else {
throw e;
}
} else {
debug(`Attempt to execute function "${func?.name}" failed, retries left: ${retryOptions.retryCount}`);
retryOptions.retryCount--;
}
}
}
}

/**
* Recursively finds all paths a given path. Returns an array of paths.
*
Expand Down Expand Up @@ -907,7 +953,13 @@ export function find(findPath: string, options?: FindOptions): string[] {

if (options.followSymbolicLinks) {
// get the realpath
let realPath: string = fs.realpathSync(item.path);
let realPath: string;
if (im._isUncPath(item.path)) {
// Sometimes there are spontaneous issues when working with unc-paths, so retries have been added for them.
realPath = retry(fs.realpathSync, [item.path], { continueOnError: false, retryCount: 5 });
} else {
realPath = fs.realpathSync(item.path);
}

// fixup the traversal chain to match the item level
while (traversalChain.length >= item.level) {
Expand Down
41 changes: 41 additions & 0 deletions node/test/isuncpathtests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import assert = require('assert');
import * as im from '../_build/internal';
import testutil = require('./testutil');

describe('Is UNC-path Tests', function () {

before(function (done) {
try {
testutil.initialize();
} catch (err) {
assert.fail('Failed to load task lib: ' + err.message);
}
done();
});

after(function () {
});

it('checks if path is unc path', (done: MochaDone) => {
this.timeout(1000);

const paths = [
{ inputPath: '\\server\\path\\to\\file', isUNC: false },
{ inputPath: '\\\\server\\path\\to\\file', isUNC: true },
{ inputPath: '\\\\\\server\\path\\to\\file', isUNC: false },
{ inputPath: '!@#$%^&*()_+', isUNC: false },
{ inputPath: '\\\\\\\\\\\\', isUNC: false },
{ inputPath: '1q2w3e4r5t6y', isUNC: false },
{ inputPath: '', isUNC: false }
];

for (let path of paths) {
assert.deepEqual(im._isUncPath(path.inputPath), path.isUNC);
}

done();
});
});
57 changes: 57 additions & 0 deletions node/test/retrytests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import assert = require('assert');
import * as tl from '../_build/task';
import testutil = require('./testutil');

describe('Retry Tests', function () {

before(function (done) {
try {
testutil.initialize();
} catch (err) {
assert.fail('Failed to load task lib: ' + err.message);
}
done();
});

after(function () {
});

it('retries to execute a function', (done: MochaDone) => {
this.timeout(1000);

const testError = Error('Test Error');

function count(num: number) {
return () => num--;
}

function fail(count: Function) {
if (count()) {
throw testError;
}

return 'completed';
}

function catchError(func: Function, args: any[]) {
try {
func(...args);
} catch (err) {
return err;
}
}

assert.deepEqual(catchError(tl.retry, [fail, [count(5)], { continueOnError: false, retryCount: 3 }]), testError);
assert.deepEqual(catchError(tl.retry, [fail, [count(5)], { continueOnError: false, retryCount: 4 }]), testError);
assert.deepEqual(tl.retry(fail, [count(5)], { continueOnError: false, retryCount: 5 }), 'completed');
assert.deepEqual(tl.retry(fail, [count(5)], { continueOnError: false, retryCount: 6 }), 'completed');
assert.deepEqual(tl.retry(fail, [count(5)], { continueOnError: true, retryCount: 3 }), undefined);
assert.deepEqual(tl.retry(() => 123, [0, 'a', 1, 'b', 2], { continueOnError: false, retryCount: 7 }), 123);
assert.deepEqual(tl.retry((a: string, b: string) => a + b, [''], { continueOnError: false, retryCount: 7 }), 'undefined');

done();
});
});
6 changes: 4 additions & 2 deletions node/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"matchtests.ts",
"filtertests.ts",
"findmatchtests.ts",
"mocktests.ts"
"mocktests.ts",
"retrytests.ts",
"isuncpathtests.ts"
]
}
}

0 comments on commit ce5aefa

Please sign in to comment.