2020-05-29 14:39:34 -07:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { InnerLogger, Log } from './logger';
|
|
|
|
import { TimeoutError } from './errors';
|
|
|
|
import { helper } from './helper';
|
|
|
|
import * as types from './types';
|
|
|
|
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';
|
|
|
|
import { getCurrentApiCall, rewriteErrorMessage } from './debug/stackTrace';
|
|
|
|
|
|
|
|
class AbortError extends Error {}
|
|
|
|
|
|
|
|
export class Progress {
|
2020-06-01 08:54:18 -07:00
|
|
|
static async runCancelableTask<T>(task: (progress: Progress) => Promise<T>, timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettings?: TimeoutSettings, apiName?: string): Promise<T> {
|
|
|
|
apiName = apiName || getCurrentApiCall();
|
|
|
|
|
|
|
|
const defaultTimeout = timeoutSettings ? timeoutSettings.timeout() : DEFAULT_TIMEOUT;
|
|
|
|
const { timeout = defaultTimeout } = timeoutOptions;
|
|
|
|
const deadline = TimeoutSettings.computeDeadline(timeout);
|
2020-05-29 14:39:34 -07:00
|
|
|
|
2020-06-01 08:54:18 -07:00
|
|
|
let rejectCancelPromise: (error: Error) => void = () => {};
|
|
|
|
const cancelPromise = new Promise<T>((resolve, x) => rejectCancelPromise = x);
|
|
|
|
const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded during ${apiName}.`);
|
|
|
|
const timer = setTimeout(() => rejectCancelPromise(timeoutError), helper.timeUntilDeadline(deadline));
|
2020-05-29 14:39:34 -07:00
|
|
|
|
2020-06-01 08:54:18 -07:00
|
|
|
let resolveCancelation = () => {};
|
|
|
|
const progress = new Progress(deadline, logger, new Promise(resolve => resolveCancelation = resolve), rejectCancelPromise, apiName);
|
2020-05-29 14:39:34 -07:00
|
|
|
try {
|
|
|
|
const promise = task(progress);
|
2020-06-01 08:54:18 -07:00
|
|
|
const result = await Promise.race([promise, cancelPromise]);
|
|
|
|
clearTimeout(timer);
|
2020-05-29 14:39:34 -07:00
|
|
|
progress._running = false;
|
|
|
|
progress._logRecording = [];
|
|
|
|
return result;
|
|
|
|
} catch (e) {
|
|
|
|
resolveCancelation();
|
2020-06-01 08:54:18 -07:00
|
|
|
rewriteErrorMessage(e, e.message + formatLogRecording(progress._logRecording, apiName));
|
|
|
|
clearTimeout(timer);
|
2020-05-29 14:39:34 -07:00
|
|
|
progress._running = false;
|
|
|
|
progress._logRecording = [];
|
|
|
|
await Promise.all(progress._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
readonly apiName: string;
|
|
|
|
readonly deadline: number; // To be removed?
|
2020-06-01 08:54:18 -07:00
|
|
|
readonly cancel: (error: Error) => void;
|
2020-05-29 14:39:34 -07:00
|
|
|
readonly _canceled: Promise<any>;
|
|
|
|
|
|
|
|
private _logger: InnerLogger;
|
|
|
|
private _logRecording: string[] = [];
|
|
|
|
private _cleanups: (() => any)[] = [];
|
|
|
|
private _running = true;
|
|
|
|
|
2020-06-01 08:54:18 -07:00
|
|
|
constructor(deadline: number, logger: InnerLogger, canceled: Promise<any>, cancel: (error: Error) => void, apiName: string) {
|
|
|
|
this.deadline = deadline;
|
|
|
|
this.apiName = apiName;
|
|
|
|
this.cancel = cancel;
|
2020-05-29 14:39:34 -07:00
|
|
|
this._canceled = canceled;
|
|
|
|
this._logger = logger;
|
|
|
|
}
|
|
|
|
|
2020-06-01 15:48:23 -07:00
|
|
|
isCanceled(): boolean {
|
|
|
|
return !this._running;
|
|
|
|
}
|
|
|
|
|
2020-05-29 14:39:34 -07:00
|
|
|
cleanupWhenCanceled(cleanup: () => any) {
|
|
|
|
if (this._running)
|
|
|
|
this._cleanups.push(cleanup);
|
|
|
|
else
|
|
|
|
runCleanup(cleanup);
|
|
|
|
}
|
|
|
|
|
|
|
|
throwIfCanceled() {
|
|
|
|
if (!this._running)
|
|
|
|
throw new AbortError();
|
|
|
|
}
|
|
|
|
|
|
|
|
race<T>(promise: Promise<T>, cleanup?: () => any): Promise<T> {
|
|
|
|
const canceled = this._canceled.then(async error => {
|
|
|
|
if (cleanup)
|
|
|
|
await runCleanup(cleanup);
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
const success = promise.then(result => {
|
|
|
|
cleanup = undefined;
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
return Promise.race<T>([success, canceled]);
|
|
|
|
}
|
|
|
|
|
|
|
|
log(log: Log, message: string | Error): void {
|
|
|
|
if (this._running)
|
|
|
|
this._logRecording.push(message.toString());
|
|
|
|
this._logger._log(log, message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function runCleanup(cleanup: () => any) {
|
|
|
|
try {
|
|
|
|
await cleanup();
|
|
|
|
} catch (e) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatLogRecording(log: string[], name: string): string {
|
2020-06-01 08:54:18 -07:00
|
|
|
if (!log.length)
|
|
|
|
return '';
|
2020-05-29 14:39:34 -07:00
|
|
|
name = ` ${name} logs `;
|
|
|
|
const headerLength = 60;
|
|
|
|
const leftLength = (headerLength - name.length) / 2;
|
|
|
|
const rightLength = headerLength - name.length - leftLength;
|
|
|
|
return `\n${'='.repeat(leftLength)}${name}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
|
|
|
|
}
|