Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: microsoft/azure-pipelines-tasks
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: Flyingliuhub/azure-pipelines-tasks
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 1 commit
  • 16 files changed
  • 1 contributor

Commits on Dec 1, 2021

  1. Verified

    This commit was signed with the committer’s verified signature.
    ChrisDenton Chris Denton
    Copy the full SHA
    fd13627 View commit details
66 changes: 66 additions & 0 deletions Tasks/Common/clienttool-common/ClientToolRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
let fs = require("fs");
let os = require("os");
import * as tl from "azure-pipelines-task-lib";
import child = require("child_process");
import stream = require("stream");
import {IExecOptions, IExecSyncResult} from "azure-pipelines-task-lib/toolrunner";

export interface IClientToolOptions {
clientToolFilePath: string;
expirationInDays: string;
indexableFileFormats: string;
personalAccessToken: string;
requestName: string;
symbolServiceUri: string;
}

export function getOptions(): IExecOptions{
let result: IExecOptions = <IExecOptions>{
cwd: process.cwd(),
env: Object.assign({}, process.env),
silent: false,
failOnStdErr: false,
ignoreReturnCode: false,
windowsVerbatimArguments: false
};
result.outStream = process.stdout as stream.Writable;
result.errStream = process.stderr as stream.Writable;
return result;
}

function getCommandString(toolPath: string, command: string[]){
let cmd: string = toolPath;
command.forEach((a: string): void => {
cmd += ` ${a}`;
});
return cmd;
}

export function runClientTool(clientToolPath: string, command: string[], execOptions: IExecOptions): IExecSyncResult{

if (tl.osType() === "Windows_NT" || clientToolPath.trim().toLowerCase().endsWith(".exe")) {
return tl.execSync(clientToolPath, command, execOptions);
}
else{
fs.chmodSync(clientToolPath, "755");

if (!execOptions.silent) {
execOptions.outStream.write(getCommandString(clientToolPath, command) + os.EOL);
}

let result = child.spawnSync(clientToolPath, command, execOptions);

if (!execOptions.silent && result.stdout && result.stdout.length > 0) {
execOptions.outStream.write(result.stdout);
}

if (!execOptions.silent && result.stderr && result.stderr.length > 0) {
execOptions.errStream.write(result.stderr);
}

let res: IExecSyncResult = <IExecSyncResult>{ code: result.status, error: result.error };
res.stdout = (result.stdout) ? result.stdout.toString() : null;
res.stderr = (result.stderr) ? result.stderr.toString() : null;
return res;
}
}
200 changes: 200 additions & 0 deletions Tasks/Common/clienttool-common/ClientToolUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import AdmZip = require('adm-zip');
import * as vsts from 'azure-devops-node-api';
import os = require("os");
import * as path from "path";
import * as tl from "azure-pipelines-task-lib";
import * as toollib from "azure-pipelines-tool-lib/tool";
import { IRequestOptions } from 'azure-devops-node-api/interfaces/common/VsoBaseInterfaces';


export function getClientToolLocation(dirName: string, toolName: string): string {
let toolPath: string = path.join(dirName, toolName);
if (tl.osType() !== "Windows_NT") {
toolPath = path.join(dirName, toolName);
}
return toolPath;
}

// This function is to apply retries generically for any unreliable network calls
export async function retryOnExceptionHelper<T>(action: () => Promise<T>, maxTries: number, retryIntervalInMilliseconds: number): Promise<T> {
while (true) {
try {
return await action();
} catch (error) {
maxTries--;
if (maxTries < 1) {
throw error;
}
tl.debug(`Network call failed. Number of retries left: ${maxTries}`);
if (error) { tl.debug(error); }
await delay(retryIntervalInMilliseconds);
}
}
}

function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export function getSystemAccessToken(): string {
tl.debug('Getting credentials for client tool');
const auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false);
if (auth.scheme === 'OAuth') {
tl.debug('Got auth token');
return auth.parameters['AccessToken'];
} else {
tl.warning('Could not determine credentials to use');
}
}

function _createExtractFolder(toolName: string, dest?: string,): string {
if (!dest) {
// create a temp dir
dest = path.join(tl.getVariable("Agent.TempDirectory"), toolName);
}
tl.mkdirP(dest);
return dest;
}

export function getSupportedArchitecture(): string {
let architecture = os.arch();

if (architecture === "x64") {
architecture = "amd64";
}

// https://github.com/nodejs/node-v0.x-archive/issues/2862
if (architecture == "ia32") {
if (process.env.PROCESSOR_ARCHITEW6432 != null && process.env.PROCESSOR_ARCHITEW6432.toUpperCase() === "AMD64") {
architecture = "amd64";
}
}

if (architecture.toLowerCase() !== "amd64") {
throw new Error(tl.loc("Error_ProcessorArchitectureNotSupported"));
}

return architecture;
}

export function getSupportedOSType(): string {

switch (tl.osType()) {
case 'Linux':
return 'linux';

case 'Windows_NT':
return 'windows';

case 'Darwin':
return 'darwin';

default:
throw Error('Not Supported OS type');
}
}

// there is a reason we do this instead of toollib.extractZip, but we don't recall what it is
// (might be Mac compatibility)
export async function extractZip(file: string, toolName: string): Promise<string> {
if (!file) {
throw new Error("parameter 'file' is required");
}
let dest = _createExtractFolder(toolName);
let zip = new AdmZip(file);
zip.extractAllTo(dest, true);
return dest;
}

export async function getClientToolFromService(serviceUri: string, accessToken: string, toolName: string) {

let osName = getSupportedOSType();
let arch = getSupportedArchitecture();

const overrideClientToolPath = tl.getVariable(toolName + ".OverrideClientToolPath");
if (overrideClientToolPath != null) {
return getClientToolLocation(overrideClientToolPath, toolName);
}

const blobstoreAreaName = "clienttools";
const blobstoreAreaId = "187ec90d-dd1e-4ec6-8c57-937d979261e5";
const ApiVersion = "5.0-preview";

const blobstoreConnection = getWebApiWithProxy(serviceUri, accessToken);

const clientToolGetUrl = await blobstoreConnection.vsoClient.getVersioningData(ApiVersion, blobstoreAreaName, blobstoreAreaId, { toolName }, { osName, arch });

const clientToolUri = await blobstoreConnection.rest.get(clientToolGetUrl.requestUrl);

if (clientToolUri.statusCode !== 200) {
tl.debug(tl.loc("Error_UnexpectedErrorFailedToGetToolMetadata", clientToolUri.result.toString()));
throw new Error(tl.loc("Error_UnexpectedErrorFailedToGetToolMetadata", clientToolGetUrl.requestUrl));
}

let clientToolPath = toollib.findLocalTool(toolName, clientToolUri.result['version']);
if (!clientToolPath) {
tl.debug(tl.loc("Info_DownloadingClientTool", clientToolUri.result['uri']));

const zippedToolsDir: string = await retryOnExceptionHelper(() => toollib.downloadTool(clientToolUri.result['uri']), 3, 1000);

tl.debug("Downloaded zipped client tool to " + zippedToolsDir);
const unzippedToolsDir = await extractZip(zippedToolsDir, toolName);

clientToolPath = await toollib.cacheDir(unzippedToolsDir, toolName, clientToolUri.result['version']);
} else {
tl.debug(tl.loc("Info_ResolvedToolFromCache", clientToolPath));
}
return getClientToolLocation(clientToolPath, toolName);
}

// trim the given character if it exists in the end of string.
export function trimEnd(data: string, trimChar: string) {
if (!trimChar || !data) {
return data;
}

if (data.endsWith(trimChar)) {
return data.substring(0, data.length - trimChar.length);
} else {
return data;
}
}

export function getWebApiWithProxy(serviceUri: string, accessToken?: string): vsts.WebApi {
if (!accessToken) {
accessToken = getSystemAccessToken();
}

const credentialHandler = vsts.getBasicHandler('vsts', accessToken);
const options: IRequestOptions = {
proxy: tl.getHttpProxyConfiguration(serviceUri),
allowRetries: true,
maxRetries: 5
};
const webApi = new vsts.WebApi(serviceUri, credentialHandler, options);
tl.debug(`Created webApi client for ${serviceUri}; options: ${JSON.stringify(options)}`);
return webApi;
}

export async function getBlobstoreUriFromBaseServiceUri(serviceUri: string, accesstoken: string): Promise<string> {
const blobAreaId = '5294ef93-12a1-4d13-8671-9d9d014072c8';

return getServiceUriFromAreaId(serviceUri, accesstoken, blobAreaId);
}

// Getting service urls from resource areas api
export async function getServiceUriFromAreaId(serviceUri: string, accessToken: string, areaId: string): Promise<string> {
const serverType = tl.getVariable('System.ServerType');
if (!serverType || serverType.toLowerCase() !== 'hosted') {
return serviceUri;
}

const webApi = getWebApiWithProxy(serviceUri, accessToken);
const locationApi = await webApi.getLocationsApi();

tl.debug(`Getting URI for area ID ${areaId} from ${serviceUri}`);
const resourceArea = await retryOnExceptionHelper(() => locationApi.getResourceArea(areaId), 3, 1000);
tl.debug(`Found resource area with locationUrl: ${resourceArea && resourceArea.locationUrl}`);

return resourceArea.locationUrl;
}
80 changes: 80 additions & 0 deletions Tasks/Common/clienttool-common/Tests/ClientToolMockHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import tmrm = require('azure-pipelines-task-lib/mock-run');

export function registerClientToolUtilitiesMock(tmr: tmrm.TaskMockRunner, toolPath: string) {
tmr.registerMock('clienttool-common/ClientToolUtilities', {
getClientToolFromService: function (serviceUri, accessToken, toolName) {
return toolPath;
},
getBlobstoreUriFromBaseServiceUri: function (serviceUri: string, accesstoken: string) {
return serviceUri + "/blobstore"
},
getWebApiWithProxy: function (serviceUri: string, accessToken?: string) {
return {
vsoClient: {
getVersioningData: async function (ApiVersion: string, PackagingAreaName: string, PackageAreaId: string, Obj) {
return { requestUrl: 'foobar' };
}
}
}
},
getSystemAccessToken: function () {
return "token";
},
retryOnExceptionHelper: async function <T>(action: () => Promise<T>, maxTries: number, retryIntervalInMilliseconds: number) {
return await action();
},
retryOnNullOrExceptionHelper: async function <T>(action: () => Promise<T>, maxTries: number, retryIntervalInMilliseconds: number) {
return await action();
},
trimEnd: function (data: string, trimChar: string) {
return data;
}
});
}

export function registerClientToolRunnerMock(tmr: tmrm.TaskMockRunner) {
var mtt = require('azure-pipelines-task-lib/mock-toolrunner');
tmr.registerMock('clienttool-common/ClientToolRunner', {
getOptions: function () {
return {
cwd: process.cwd(),
env: Object.assign({}, process.env),
silent: false,
failOnStdErr: false,
ignoreReturnCode: false,
windowsVerbatimArguments: false
}
},
runClientTool: function (clientToolPath: string, command: string[], execOptions) {
var tr = new mtt.ToolRunner(clientToolPath)
tr.arg(command);
return tr.execSync(execOptions);
}
});
}

export function registerOtherMock(tmr: tmrm.TaskMockRunner) {
class MockStats {
isFile = () => {
return true;
};
};
const fsAnswers = {
writeFileSync: function (filePath, contents) {
},
existsSync: function (filePath, contents) {
return true;
},
readFileSync: function (filePath) {
return 'contents';
},
statSync: function (filePath) {
let s: MockStats = new MockStats();
return s;
},
chmodSync: function (filePath, string) {
}
};

tmr.registerMock('fs', fsAnswers);
}
22 changes: 22 additions & 0 deletions Tasks/Common/clienttool-common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "clienttool-common",
"version": "1.0.0",
"description": "Azure Pipelines client tool Tasks Common",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Microsoft Corporation",
"category": "Package",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/azure-pipelines-tasks"
},
"license": "MIT",
"dependencies": {
"@types/mocha": "5.2.6",
"@types/node": "10.12.9",
"azure-devops-node-api": "8.0.0",
"azure-pipelines-task-lib": "2.8.0",
"azure-pipelines-tool-lib": "0.12.0"
}
}
12 changes: 12 additions & 0 deletions Tasks/Common/clienttool-common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"declaration": true,
"noImplicitAny": false,
"sourceMap": false
},
"exclude": [
"node_modules"
]
}
Loading