Feature: Better logging for package installation (#4360)

This commit is contained in:
Timothee Guerin 2021-11-15 12:18:46 -08:00 коммит произвёл GitHub
Родитель 511bc2ed08
Коммит 7b20086e16
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
33 изменённых файлов: 723 добавлений и 400 удалений

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/common",
"comment": "**Added** progress bar reporting to the logger",
"type": "minor"
}
],
"packageName": "@autorest/common",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/configuration",
"comment": "Uptake changes to the extension loader and report installation progress",
"type": "minor"
}
],
"packageName": "@autorest/configuration",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/core",
"comment": "Uptake change to @autorest/extension package.",
"type": "minor"
}
],
"packageName": "@autorest/core",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/test-utils",
"comment": "Uptake change to logger",
"type": "minor"
}
],
"packageName": "@autorest/test-utils",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@azure-tools/extension",
"comment": "**Added** Progress tracking for installation and imrpvoed error reporting",
"type": "minor"
}
],
"packageName": "@azure-tools/extension",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "autorest",
"comment": "Uptake change to @autorest/extension package.",
"type": "minor"
}
],
"packageName": "autorest",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -30,6 +30,7 @@ dependencies:
'@rush-temp/test-utils': file:projects/test-utils.tgz_prettier@2.3.2+ts-node@9.1.1
'@rush-temp/yaml': file:projects/yaml.tgz_prettier@2.3.2+ts-node@9.1.1
'@types/body-parser': 1.19.1
'@types/cli-progress': 3.9.2
'@types/command-exists': 1.2.0
'@types/commonmark': 0.27.5
'@types/deep-equal': 1.0.1
@ -58,6 +59,7 @@ dependencies:
ajv-formats: 2.1.1
body-parser: 1.19.0
chalk: 4.1.2
cli-progress: 3.9.1
command-exists: 1.2.9
commonmark: 0.27.0
compare-versions: 3.6.0
@ -938,6 +940,12 @@ packages:
dev: false
resolution:
integrity: sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==
/@types/cli-progress/3.9.2:
dependencies:
'@types/node': 14.14.45
dev: false
resolution:
integrity: sha512-VO5/X5Ij+oVgEVjg5u0IXVe3JQSKJX+Ev8C5x+0hPy0AuWyW+bF8tbajR7cPFnDGhs7pidztcac+ccrDtk5teA==
/@types/command-exists/1.2.0:
dev: false
resolution:
@ -2243,6 +2251,15 @@ packages:
node: '>=4'
resolution:
integrity: sha1-jffHquUf02h06PjQW5GAvBGj/tc=
/cli-progress/3.9.1:
dependencies:
colors: 1.4.0
string-width: 4.2.2
dev: false
engines:
node: '>=4'
resolution:
integrity: sha512-AXxiCe2a0Lm0VN+9L0jzmfQSkcZm5EYspfqXKaSIQKqIk+0hnkZ3/v1E9B39mkD6vYhKih3c/RPsJBSwq9O99Q==
/cliui/5.0.0:
dependencies:
string-width: 3.1.0
@ -9191,7 +9208,7 @@ packages:
peerDependencies:
ts-node: '*'
resolution:
integrity: sha512-OVeXR+8NDnJgJkJyUTzM8Js4g6tQ0RHG/MdP9LUV72j30aqQgXEGcGmyzCEDB28NfvLj3g+ifEku2P5G2XLD3A==
integrity: sha512-MpNqbnbGTn2/x4A+5vzCiGLqBrb3oVtjif9njDDo80AMkwVFDi+/2jkQ7PPOWK22W9am3CPEdeZqBRwjM2lqtA==
tarball: file:projects/autorest.tgz
version: 0.0.0
file:projects/cadl.tgz_ts-node@9.1.1+webpack@5.40.0:
@ -9226,7 +9243,7 @@ packages:
ts-node: '*'
webpack: '*'
resolution:
integrity: sha512-vxvPXgPJTt0yrvFH5oFumu7Ivqdtw4hww5d7nSoy6ICTnPdoZSVK6Pa4Zbpin1+9fpBZAMIu+SBxBfcYZfbPZQ==
integrity: sha512-mkJpA8Z3+oRed++5iLDcFJZhEy/WoxartWsu8P8I5Wb+38Fna/0hU4JEF/oF4Wck9Kn5aEtqti28Z4DGICdV6g==
tarball: file:projects/cadl.tgz
version: 0.0.0
file:projects/codegen.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9285,7 +9302,7 @@ packages:
peerDependencies:
prettier: '*'
resolution:
integrity: sha512-mFz2VfiQVtof+igNYv0kUIsWBL4yKDcNowdGLB46upeKfjS6IoZtJww0HaywCS8JbY1yBOA00TjtFyeCV3x86A==
integrity: sha512-QCZ1c+ryLL7x7CKKccHrYVKJXittu3jcDkJAM3IOAaJ6Qgikqcy/5FkHlLIr2zc44N+v+gqlgUk2rAYt5JxlpQ==
tarball: file:projects/codemodel.tgz
version: 0.0.0
file:projects/common.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9312,7 +9329,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-VlWPj+ryuGsUB3pc/obhBVIVpM/4S5WD+c5Tr0UYU1iumP0jKmRLyXA0iEOSqa1FyrZQZd4YEQjL0CXRDLunIQ==
integrity: sha512-1Hw9B8MqUGJv3N3E0nc/ihlLR5zn1FnQkX0WeTpWq8m2AybiEkQvCKBhp+seMJFrJnzbrhn+IlAQub18zTrI/Q==
tarball: file:projects/common.tgz
version: 0.0.0
file:projects/compare.tgz_prettier@2.3.2:
@ -9347,7 +9364,7 @@ packages:
peerDependencies:
prettier: '*'
resolution:
integrity: sha512-TF01wD/AzM6WP4tkyMk8zI4UNKfBBXa0kmh18GvBMWBiLa1+4dI+LTJ9f2qUwlSIi5WMiNkvwbWVDjRfxGwajw==
integrity: sha512-m6OwVTxymkE9FAdUpQgQ5064myIJBvaBPMmpphLCGJtk/HvRpHcMJBdMvuGmoQDiuiAwGk2Dx7V9csQO2N5Qvg==
tarball: file:projects/compare.tgz
version: 0.0.0
file:projects/configuration.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9380,7 +9397,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-ixI+T6Q45pf5aZy9px0d/Zr73UMZ5GUYNQ6G/I3tLTagI9we6rcaRUefSHk66xiO0KrMrXK3njM7rFMLJfmCxw==
integrity: sha512-UZ53gfYOZaoEX2tIDjPUiKfj1wfI64Gbzw4cTFpw7q5gZX/YTG/20NK/1TntyP88Q7H8+p0wNCRiJlwKBODoPQ==
tarball: file:projects/configuration.tgz
version: 0.0.0
file:projects/core.tgz_ts-node@9.1.1:
@ -9389,6 +9406,7 @@ packages:
'@azure-tools/object-comparison': 3.0.253
'@azure-tools/tasks': 3.0.255
'@azure-tools/uri': 3.1.1
'@types/cli-progress': 3.9.2
'@types/commonmark': 0.27.5
'@types/jest': 26.0.24
'@types/jsonpath': 0.2.0
@ -9400,6 +9418,7 @@ packages:
ajv: 8.6.2
ajv-errors: 3.0.0_ajv@8.6.2
ajv-formats: 2.1.1
cli-progress: 3.9.1
commonmark: 0.27.0
compare-versions: 3.6.0
copy-webpack-plugin: 7.0.0_webpack@5.40.0
@ -9434,7 +9453,7 @@ packages:
peerDependencies:
ts-node: '*'
resolution:
integrity: sha512-TkL/z4SM5fGGP6RHyCrsChzkD2zx2LyR4endGy0IhQNklDwbxzjuU9soFiQ9HfOfyRunmN4F/AxLGa0a47iXEQ==
integrity: sha512-hVmBc+yNi5YVO5C9tN11uq2B9rZ7TmwmkGXCbOG7fh0iFyE5+V4YyShK1irpUSYRhN4/yoJfu2HqRFsj6pI+4w==
tarball: file:projects/core.tgz
version: 0.0.0
file:projects/datastore.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9468,7 +9487,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-TbjyFxINESWXHSXfu724HtdQcUEkL3/X4TkOPQCGlF4KXsegpg4t33Ji6qLmXA3amutFwS+7gqXnZX05KdEJKQ==
integrity: sha512-pkpfsGRn8PBbFx0WOghfw0JSJPY4J01etP36eCfPf5BOUSVQP0VfhYtuajDoqd89Amv+Legq1DFakLsRWJZKQw==
tarball: file:projects/datastore.tgz
version: 0.0.0
file:projects/deduplication.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9498,7 +9517,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-MYQ8DPYd8PRILUdoR1N+jrLvOJuM3W8wm2AK3xez8Z1Y2UyV/bYNhaww9aYB/q5BwXMl6c0x19sOKacKGm3ybw==
integrity: sha512-OLaxADMpKCeiBvlh/gr55kLVVB7oEOpeO55cLpYvMgnzP59j4VmOPQttEvo8ZmMzBr0iNMNoc1HW1uBNMWBFXA==
tarball: file:projects/deduplication.tgz
version: 0.0.0
file:projects/extension-base.tgz_prettier@2.3.2:
@ -9523,7 +9542,7 @@ packages:
peerDependencies:
prettier: '*'
resolution:
integrity: sha512-IY5+GFmGnLIMllhmV0yuTR6ShoQWsCIjymoXxTmVGY5wxy9WdRuYCpZu/MKkrebu/nD2VHIyvpXaf1JqwI12tQ==
integrity: sha512-m1X/JYvivbdTric3atlDqaqHLCbJTECxX96dGHiTRcbIM56jROVbSLsw4oe737MTx0ShOUUEq+vZK7gUdRgZSw==
tarball: file:projects/extension-base.tgz
version: 0.0.0
file:projects/extension.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9737,7 +9756,7 @@ packages:
peerDependencies:
ts-node: '*'
resolution:
integrity: sha512-T/lhdQdhW3dDTLx/hKUeJqxeXIMlQu1ND+DLUwsXY3lc/NRnP6rC5V+1RGTMjiMUuzb6VnaCkJWw5HwOzUfMsQ==
integrity: sha512-wbAs7xv5ovUIfZgMjBg4Jbpym2NOEG79mo/FbGadIvnSmZEHh2h5/ncm9IfYcDJsEjUIN3pVhPW8c/7514Ivhw==
tarball: file:projects/modelerfour.tgz
version: 0.0.0
file:projects/oai2-to-oai3.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9766,7 +9785,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-6YP89sECKf2MF6e9cl8NYvhXIU8OeEi6NW5G61thiZzmB8mwOulJ2yNraLs8S150feF4nWbJtfJu1CvMi0iIPg==
integrity: sha512-w7ZHAKyQ3PnoCkSaq/W5SDWVQBORIddI4yvCvGTLoeTi4axtImIGhZ8cncslZocFBkKsW8lScnYJ1AQHtXMkJA==
tarball: file:projects/oai2-to-oai3.tgz
version: 0.0.0
file:projects/openapi.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9817,7 +9836,7 @@ packages:
peerDependencies:
prettier: '*'
resolution:
integrity: sha512-qyjJL+5/hyJbeCF38NSkgiNd5TAK/i5ibrc3MH9g8YLiFcn5t/fmF6gQ44hAP0XLYPyAzqlTJu7hhZhqDE44QA==
integrity: sha512-b5uy+rnnvde68qRG9NkFEncryV0ZNel4NZh5HRiFANmC2dbIf4OWpVTJPTIPXo1AJT4Is43e6fSIu8copnn7Tw==
tarball: file:projects/test-public-packages.tgz
version: 0.0.0
file:projects/test-utils.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9839,7 +9858,7 @@ packages:
prettier: '*'
ts-node: '*'
resolution:
integrity: sha512-KgD9TLvgY49dTKkpIcjQwxn6z0efwqwj6COGl/8yOsQxDA5neMoBwaBtHm6edMOv5yLgnX9gd4CtYeP8Bzw2uA==
integrity: sha512-whu9IiGZUbreZKk3Y+hhawjOOucfiBiFlnaRaFG36E9lK9X9uvmkMunHsS1zM2mqRKuiYz2uRqP7cSym7jGSUg==
tarball: file:projects/test-utils.tgz
version: 0.0.0
file:projects/yaml.tgz_prettier@2.3.2+ts-node@9.1.1:
@ -9905,6 +9924,7 @@ specifiers:
'@rush-temp/test-utils': file:./projects/test-utils.tgz
'@rush-temp/yaml': file:./projects/yaml.tgz
'@types/body-parser': ^1.19.0
'@types/cli-progress': ~3.9.2
'@types/command-exists': ~1.2.0
'@types/commonmark': ^0.27.0
'@types/deep-equal': ^1.0.1
@ -9933,6 +9953,7 @@ specifiers:
ajv-formats: ^2.1.0
body-parser: ^1.19.0
chalk: ^4.1.0
cli-progress: ~3.9.1
command-exists: ~1.2.9
commonmark: ^0.27.0
compare-versions: ^3.4.0

Просмотреть файл

@ -61,6 +61,7 @@
"chalk": "^4.1.0",
"copy-webpack-plugin": "^7.0.0",
"cpy-cli": "~2.0.0",
"eslint-plugin-jest": "~24.3.2",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-prettier": "~3.4.0",
"eslint-plugin-unicorn": "~33.0.1",

Просмотреть файл

@ -8,7 +8,8 @@ import "source-map-support/register";
const cwd = process.cwd();
import { AutorestSyncLogger, ConsoleLoggerSink } from "@autorest/common";
import { AutorestSyncLogger, ConsoleLoggerSink, FilterLogger } from "@autorest/common";
import { getLogLevel } from "@autorest/configuration";
import chalk from "chalk";
import { clearTempData } from "./actions";
import { parseAutorestArgs } from "./args";
@ -108,7 +109,10 @@ async function main() {
logger.info(`AutoRest core version selected from configuration: ${chalk.yellow.bold(config.version)}.`);
}
const coreVersionPath = await resolveCoreVersion(config);
const coreVersionPath = await resolveCoreVersion(
logger.with(new FilterLogger({ level: getLogLevel({ ...args, ...config }) })),
config,
);
// let's strip the extra stuff from the command line before we require the core module.
const newArgs: string[] = [];

Просмотреть файл

@ -5,14 +5,12 @@ import { spawn } from "child_process";
import { lookup } from "dns";
import { mkdtempSync, rmdirSync } from "fs";
import { homedir, tmpdir } from "os";
import { join } from "path";
import { IAutorestLogger } from "@autorest/common";
import { AutorestConfiguration } from "@autorest/configuration";
import { isFile, mkdir, isDirectory } from "@azure-tools/async-io";
import { Extension, ExtensionManager, Package } from "@azure-tools/extension";
import { Exception, When } from "@azure-tools/tasks";
import * as semver from "semver";
import { AutorestArgs } from "./args";
import { VERSION } from "./constants";
@ -227,6 +225,7 @@ export async function ensureAutorestHome() {
}
export async function selectVersion(
logger: IAutorestLogger,
requestedVersion: string,
force: boolean,
minimumVersion?: string,
@ -240,20 +239,14 @@ export async function selectVersion(
}
if (currentVersion) {
if (args.debug) {
console.log(`The most recent installed version is ${currentVersion.version}`);
}
logger.debug(`The most recent installed version is ${currentVersion.version}`);
if (requestedVersion === "latest-installed" || (requestedVersion === "latest" && false == (await networkEnabled))) {
if (args.debug) {
console.log(`requesting current version '${currentVersion.version}'`);
}
logger.debug(`requesting current version '${currentVersion.version}'`);
requestedVersion = currentVersion.version;
}
} else {
if (args.debug) {
console.log(`No ${newCorePackage} (or ${oldCorePackage}) is installed.`);
}
logger.debug(`No ${newCorePackage} (or ${oldCorePackage}) is installed.`);
}
let selectedVersion: Extension | null = null;
@ -267,9 +260,7 @@ export async function selectVersion(
// is the requested version installed?
if (!selectedVersion || force) {
if (!force) {
if (args.debug) {
console.log(`${requestedVersion} was not satisfied directly by a previous installation.`);
}
logger.debug(`${requestedVersion} was not satisfied directly by a previous installation.`);
}
// if it's not a file, and the network isn't available, we can't continue.
@ -336,13 +327,16 @@ export async function selectVersion(
console.log(`**Installing package** ${corePackageName}@${pkg.version}\n[This will take a few moments...]`);
}
// @autorest/core install too fast and this doesn't look good right now as Yarn doesn't give info fast enough.
// If we migrate to yarn v2 with the api we might be able to get more info and reenable that
// const progress = logger.startProgress("installing core...");
selectedVersion = await (
await extensionManager
).installPackage(pkg, force, 5 * 60 * 1000, (installer) =>
installer.Message.Subscribe((s, m) => {
if (args.debug) console.log(`Installer: ${m}`);
}),
);
).installPackage(pkg, force, 5 * 60 * 1000, (status) => {
// progress.update({ ...status });
});
// progress.stop();
if (args.debug) {
console.log(`Extension location: ${selectedVersion.packageJsonPath}`);
}

Просмотреть файл

@ -10,6 +10,7 @@
// everything else.
import { resolve } from "path";
import { IAutorestLogger } from "@autorest/common";
import { GenerationResults, IFileSystem, AutoRest as IAutoRest } from "autorest-core";
import { LanguageClient } from "vscode-languageclient";
@ -76,29 +77,6 @@ let coreModule: any = undefined;
let busy = false;
let modulePath: string | undefined = undefined;
/**
* Returns the language service entrypoint for autorest-core, bootstrapping the core if necessary
*
* If initialize has already been called, then it returns the version that was initialized, regardless of parameters
*
* @param requestedVersion an npm package reference for the version requested @see {@link https://docs.npmjs.com/cli/install#description}
*
* @param minimumVersion - a semver string representing the lowest autorest- core version that is considered acceptable.
*
* @see { @link initialize }
*/
export async function getLanguageServiceEntrypoint(
requestedVersion = "latest-installed",
minimumVersion?: string,
): Promise<string | undefined> {
if (!modulePath && !busy) {
// if we haven't already got autorest-core, let's do that now with the default settings.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await initialize(requestedVersion, minimumVersion);
}
return resolveEntrypoint(modulePath!, "language-service");
}
/**
* Returns the command-line application entrypoint for autorest-core, bootstrapping the core if necessary
*
@ -111,13 +89,14 @@ export async function getLanguageServiceEntrypoint(
* @see {@link initialize}
* */
export async function getApplicationEntrypoint(
logger: IAutorestLogger,
requestedVersion = "latest-installed",
minimumVersion?: string,
): Promise<string | undefined> {
if (!modulePath && !busy) {
// if we haven't already got autorest-core, let's do that now with the default settings.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await initialize(requestedVersion, minimumVersion);
await initialize(logger, requestedVersion, minimumVersion);
}
return resolveEntrypoint(modulePath!, "app");
}
@ -137,7 +116,11 @@ export async function getApplicationEntrypoint(
*
* @param minimumVersion - a semver string representing the lowest autorest-core version that is considered acceptable.
*/
export async function initialize(requestedVersion = "latest-installed", minimumVersion?: string) {
export async function initialize(
logger: IAutorestLogger,
requestedVersion = "latest-installed",
minimumVersion?: string,
) {
if (modulePath) {
return;
}
@ -166,7 +149,7 @@ export async function initialize(requestedVersion = "latest-installed", minimumV
// logic to resolve and optionally install a autorest core package.
// will throw if it's not doable.
const selectedVersion = await selectVersion(requestedVersion, false, minimumVersion);
const selectedVersion = await selectVersion(logger, requestedVersion, false, minimumVersion);
modulePath = await resolveEntrypoint(await selectedVersion.modulePath, "module");
if (!modulePath) {
rejectAutoRest(
@ -180,10 +163,10 @@ export async function initialize(requestedVersion = "latest-installed", minimumV
}
/** Bootstraps the core module if it's not already done and returns the AutoRest class. */
async function ensureCoreLoaded(): Promise<IAutoRest> {
async function ensureCoreLoaded(logger: IAutorestLogger): Promise<IAutoRest> {
if (!modulePath && !busy) {
// if we haven't already got autorest-core, let's do that now with the default settings.
await initialize();
await initialize(logger);
}
if (modulePath && !coreModule) {
@ -209,10 +192,14 @@ async function ensureCoreLoaded(): Promise<IAutoRest> {
*
* @param configFileOrFolderUri - a URI pointing to the folder or autorest configuration file
*/
export async function create(fileSystem?: IFileSystem, configFileOrFolderUri?: string): Promise<AutoRest> {
export async function create(
logger: IAutorestLogger,
fileSystem?: IFileSystem,
configFileOrFolderUri?: string,
): Promise<AutoRest> {
if (!modulePath && !busy) {
// if we haven't already got autorest-core, let's do that now with the default settings.
await initialize();
await initialize(logger);
}
if (modulePath && !coreModule) {
@ -235,8 +222,8 @@ export async function create(fileSystem?: IFileSystem, configFileOrFolderUri?: s
*
* @param content - the document content to evaluate
*/
export async function isOpenApiDocument(content: string): Promise<boolean> {
await ensureCoreLoaded();
export async function isOpenApiDocument(logger: IAutorestLogger, content: string): Promise<boolean> {
await ensureCoreLoaded(logger);
return coreModule.IsOpenApiDocument(content);
}
@ -250,8 +237,8 @@ export async function isOpenApiDocument(content: string): Promise<boolean> {
*
* @see {@link DocumentType}
*/
export async function identifyDocument(content: string): Promise<DocumentType> {
await ensureCoreLoaded();
export async function identifyDocument(logger: IAutorestLogger, content: string): Promise<DocumentType> {
await ensureCoreLoaded(logger);
return await coreModule.IdentifyDocument(content);
}
@ -261,124 +248,7 @@ export async function identifyDocument(content: string): Promise<DocumentType> {
*
* @returns the content as a JSON string (not a JSON DOM)
*/
export async function toJSON(content: string): Promise<string> {
await ensureCoreLoaded();
export async function toJSON(logger: IAutorestLogger, content: string): Promise<string> {
await ensureCoreLoaded(logger);
return await coreModule.LiterateToJson(content);
}
/** This is a convenience class for accessing the requests supported by AutoRest when used as a language service */
export class AutoRestLanguageService {
/**
* Represents a convenience layer on the remote language service functions (on top of LSP-defined functions)
*
* @constructor
*
* this requires a reference to the language client so that the methods can await the onReady signal
* before attempting to send requests.
*/
public constructor(private languageClient: LanguageClient) {}
/**
* Runs autorest to process a file
*
* @param documentUri The OpenApi document or AutoRest configuration file to use for the generation
*
* @param language The language to generate code for. (This is a convenience; it could have been expressed in the configuration)
*
* @param configuration Additional configuration to pass to AutoRest -- this overrides any defaults or content in the configuration file
* @returns async: a 'generated' object containg the output from the generation run.
* @see generated
*
*/
public async generate(documentUri: string, language: string, configuration: any): Promise<GenerationResults> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<GenerationResults>("generate", {
documentUri: documentUri,
language: language,
configuration: configuration,
});
}
/**
* Determines if a file is an OpenAPI document (2.0)
*
* @param contentOrUri either a URL to a file on disk or http/s, or the content of a file itself.
* @returns async:
* true - the file is an OpenAPI 2.0 document
* false - the file was not recognized.
*/
public async isOpenApiDocument(contentOrUri: string): Promise<boolean> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<boolean>("isOpenApiDocument", { contentOrUri: contentOrUri });
}
/**
* Determines if a file is an AutoRest configuration file (checks for the magic string `\n> see https://aka.ms/autorest` )
*
* @param contentOrUri either a URL to a file on disk or http/s, or the content of a file itself.
* @returns async:
* true - the file is an autorest configuration file
* false - the file was not recognized.
*/
public async isConfigurationDocument(contentOrUri: string): Promise<boolean> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<boolean>("isConfigurationDocument", { contentOrUri: contentOrUri });
}
/**
* Returns the file as a JSON string. This can be a .YAML, .MD or .JSON file to begin with.
*
* @param contentOrUri either a URL to a file on disk or http/s, or the content of a file itself.
* @returns async: string containing the file as JSON
*/
public async toJSON(contentOrUri: string): Promise<string> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<string>("toJSON", { contentOrUri: contentOrUri });
}
/**
* Finds the configuration file for a given document URI.
*
* @param documentUri the URL to a file on disk or http/s. The passed in file can be an OpenAPI file or an AutoRest configuration file.
* @returns async: the URI to the configuration file or an empty string if no configuration could be found.
*
*/
public async detectConfigurationFile(documentUri: string): Promise<string> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<string>("detectConfigurationFile", { documentUri: documentUri });
}
/**
* Determines if a file is an OpenAPI document or a configuration file in one attempt.
*
* @param contentOrUri either a URL to a file on disk or http/s, or the content of a file itself.
* @returns async:
* true - the file is a configuration file or OpenAPI (2.0) file
* false - the file was not recognized.
*/
public async isSupportedDocument(languageId: string, contentOrUri: string): Promise<boolean> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<boolean>("isSupportedDocument", {
languageId: languageId,
contentOrUri: contentOrUri,
});
}
public async identifyDocument(contentOrUri: string): Promise<DocumentType> {
// don't call before the client is ready.
await this.languageClient.onReady();
return await this.languageClient.sendRequest<DocumentType>("identifyDocument", { contentOrUri: contentOrUri });
}
}

Просмотреть файл

@ -41,7 +41,7 @@ export async function loadConfig(sink: LoggerSink, args: AutorestArgs): Promise<
});
const loader = new ConfigurationLoader(logger, defaultConfigUri, configFileOrFolder, {
extensionManager: await extensionManager,
// extensionManager: await extensionManager,
});
try {
const { config } = await loader.load([args], true);
@ -77,7 +77,10 @@ export async function resolvePathForLocalVersion(requestedVersion: string | null
return undefined;
}
export async function resolveCoreVersion(config: AutorestNormalizedConfiguration = {}): Promise<string> {
export async function resolveCoreVersion(
logger: IAutorestLogger,
config: AutorestNormalizedConfiguration = {},
): Promise<string> {
const requestedVersion: string = getRequestedCoreVersion(config) ?? "latest-installed";
const localVersion = await resolvePathForLocalVersion(config.version ? requestedVersion : null);
@ -88,7 +91,7 @@ export async function resolveCoreVersion(config: AutorestNormalizedConfiguration
// failing that, we'll continue on and see if NPM can do something with the version.
if (config.debug) {
// eslint-disable-next-line no-console
console.log(`Network Enabled: ${await networkEnabled}`);
logger.debug(`Network Enabled: ${await networkEnabled}`);
}
// wait for the bootstrapper check to finish.
@ -96,7 +99,7 @@ export async function resolveCoreVersion(config: AutorestNormalizedConfiguration
// logic to resolve and optionally install a autorest core package.
// will throw if it's not doable.
const selectedVersion = await selectVersion(requestedVersion, config.debugger);
const selectedVersion = await selectVersion(logger, requestedVersion, config.debugger);
return selectedVersion.modulePath;
}

Просмотреть файл

@ -11,13 +11,12 @@ import {
color,
ConsoleLogger,
FilterLogger,
AutorestLogger,
AutorestSyncLogger,
Exception,
IAutorestLogger,
} from "@autorest/common";
import { AutorestCliArgs, parseAutorestCliArgs, getLogLevel } from "@autorest/configuration";
import { AutorestCliArgs, parseAutorestCliArgs } from "@autorest/configuration";
EventEmitter.defaultMaxListeners = 100;
process.env["ELECTRON_RUN_AS_NODE"] = "1";
delete process.env["ELECTRON_NO_ATTACH_CONSOLE"];
@ -41,7 +40,6 @@ import { printAutorestHelp } from "./commands";
import { Artifact } from "./lib/artifact";
import { AutoRest, IsOpenApiDocument, Shutdown } from "./lib/autorest-core";
import { VERSION } from "./lib/constants";
import { getLogLevel } from "./lib/context";
let verbose = false;
let debug = false;

Просмотреть файл

@ -10,6 +10,7 @@ import {
AutorestConfiguration,
AutorestRawConfiguration,
ConfigurationLoader,
getLogLevel,
getNestedConfiguration,
mergeConfigurations,
ResolvedExtension,
@ -21,7 +22,7 @@ import { last } from "lodash";
import { AppRoot } from "../constants";
import { AutoRestExtension } from "../pipeline/plugin-endpoint";
import { StatsCollector } from "../stats";
import { AutorestContext, getLogLevel } from "./autorest-context";
import { AutorestContext } from "./autorest-context";
import { MessageEmitter } from "./message-emitter";
const inWebpack = typeof __webpack_require__ === "function";

Просмотреть файл

@ -17,6 +17,7 @@ import {
AutorestConfiguration,
arrayOf,
extendAutorestConfiguration,
getLogLevel,
} from "@autorest/configuration";
import { DataStore, CachingFileSystem } from "@azure-tools/datastore";
@ -32,7 +33,7 @@ import { MessageEmitter } from "./message-emitter";
export class AutorestContext implements IAutorestLogger {
public config: AutorestConfiguration;
public configFileFolderUri: string;
private logger: AutorestLogger;
public logger: AutorestLogger;
private originalLogger: AutorestLogger;
public constructor(
@ -90,6 +91,10 @@ export class AutorestContext implements IAutorestLogger {
this.logger.log(log);
}
public startProgress(initialName?: string) {
return this.logger.startProgress(initialName);
}
public get diagnostics() {
return this.logger.diagnostics;
}
@ -251,10 +256,6 @@ export class AutorestContext implements IAutorestLogger {
}
}
export function getLogLevel(config: AutorestNormalizedConfiguration): LogLevel {
return config.debug ? "debug" : config.verbose ? "verbose" : config.level ?? "information";
}
export function getLogSuppressions(config: AutorestConfiguration): LogSuppression[] {
const legacySuppressions: LogSuppression[] = resolveDirectives(config, (x) => x.suppress.length > 0).map((x) => {
return {

Просмотреть файл

@ -32,6 +32,7 @@
"@types/node": "~14.14.20",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"@types/cli-progress": "~3.9.2",
"eslint-plugin-jest": "~24.3.2",
"eslint-plugin-node": "~11.1.0",
"eslint-plugin-prettier": "~3.4.0",
@ -47,6 +48,7 @@
"@azure-tools/yaml": "~1.0.0",
"@azure-tools/json": "~1.2.0",
"@azure/logger": "^1.0.2",
"chalk": "^4.1.0"
"chalk": "^4.1.0",
"cli-progress": "~3.9.1"
}
}

Просмотреть файл

@ -0,0 +1,149 @@
import { Presets, SingleBar } from "cli-progress";
import { ConsoleLogger } from "./console-logger-sink";
class MockTTYStream {
// class MockTTYStream implements NodeJS.WritableStream {
public readonly isTTY = true;
public currentOutput = "";
private lines: string[] = [];
private currentLine = Buffer.from("");
private cursor = 0;
public constructor() {}
public clearLine(number: string) {
this.lines.splice(-number);
this.updateOutput();
}
public write(b: Buffer | string) {
const content = b.toString();
if (content.startsWith("\x1b")) {
// Parse the reset cursor value. not really correct but does the job for now for testing.
this.cursor = 0;
return;
}
const [first, ...lines] = content.toString().split("\n");
this.currentLine = Buffer.concat([this.currentLine.slice(0, this.cursor), Buffer.from(first)]);
this.cursor = this.currentLine.length;
for (const line of lines) {
this.lines.push(this.currentLine.toString());
this.currentLine = Buffer.from(line);
this.cursor = this.currentLine.length;
}
this.updateOutput();
}
private updateOutput() {
this.currentOutput = [...this.lines, this.currentLine.toString()].join("\n");
}
}
describe("ConsoleLogger", () => {
let logger: ConsoleLogger;
let mockStream: MockTTYStream;
beforeEach(() => {
jest.resetAllMocks();
mockStream = new MockTTYStream();
});
function expectStreamWrite(line: string) {
expect(mockStream.currentOutput).toEqual(`${line}\n`);
}
describe("pretty format", () => {
beforeEach(() => {
logger = new ConsoleLogger({
stream: mockStream as any,
color: false,
timestamp: false,
progressNoTTYOutput: true,
});
});
it("log debug", () => {
logger.debug("This is some debug");
expectStreamWrite("debug | This is some debug");
});
it("log verbose", () => {
logger.verbose("This is some verbose");
expectStreamWrite("verbose | This is some verbose");
});
it("log information", () => {
logger.info("This is some information");
expectStreamWrite("info | This is some information");
});
it("log warning", () => {
logger.trackWarning({
code: "TestWarning",
message: "This is some warning",
});
expectStreamWrite("warning | TestWarning | This is some warning");
});
it("log error", () => {
logger.trackError({
code: "TestError",
message: "This is some error",
});
expectStreamWrite("error | TestError | This is some error");
});
it("log fatal", () => {
logger.fatal("This is some fatal error");
expectStreamWrite("fatal | This is some fatal error");
});
it("log progress", async () => {
const progress = logger.startProgress("MyProgress");
progress.update({ current: 10, total: 200 });
await new Promise((r) => setTimeout(r, 100));
expect(mockStream.currentOutput).toEqual("MyProgress [==--------------------------------------] 5% | 10/200");
progress.update({ current: 20, total: 200 });
await new Promise((r) => setTimeout(r, 100));
expect(mockStream.currentOutput).toEqual("MyProgress [====------------------------------------] 10% | 20/200");
progress.stop();
});
it("append logs above progress", async () => {
const progress = logger.startProgress("MyProgress");
progress.update({ current: 10, total: 200 });
await new Promise((r) => setTimeout(r, 100));
expect(mockStream.currentOutput).toEqual("MyProgress [==--------------------------------------] 5% | 10/200");
logger.info("Something happening during progress");
await new Promise((r) => setTimeout(r, 100));
expect(mockStream.currentOutput).toEqual(
"info | Something happening during progress\nMyProgress [==--------------------------------------] 5% | 10/200",
);
progress.stop();
});
});
describe("json format", () => {
beforeEach(() => {
logger = new ConsoleLogger({
stream: mockStream as any,
format: "json",
timestamp: false,
});
});
it("log debug", () => {
logger.debug("This is some debug");
expectStreamWrite('{"level":"debug","message":"This is some debug"}');
});
it("log warning", () => {
logger.trackWarning({
code: "TestWarning",
message: "This is some warning",
});
expectStreamWrite('{"level":"warning","code":"TestWarning","message":"This is some warning"}');
});
});
});

Просмотреть файл

@ -1,11 +1,23 @@
import { WriteStream } from "tty";
import progressBar from "cli-progress";
import { createLogFormatter, LogFormatter } from "./formatter";
import { AutorestSyncLogger } from "./logger";
import { LoggerSink, LogInfo } from "./types";
import { LoggerSink, LogInfo, Progress, ProgressTracker } from "./types";
export interface ConsoleLoggerSinkOptions {
/**
* Stream to use for output. (@default stdout)
*/
stream?: NodeJS.WritableStream;
format?: "json" | "regular";
color?: boolean;
timestamp?: boolean;
/**
* Enable output for non TTY output(e.g. file) for the progress file.
*/
progressNoTTYOutput?: boolean;
}
/**
@ -13,15 +25,105 @@ export interface ConsoleLoggerSinkOptions {
*/
export class ConsoleLoggerSink implements LoggerSink {
private formatter: LogFormatter;
private currentProgressBar: progressBar.MultiBar | undefined;
private bars: progressBar.SingleBar[] = [];
private pendingLogs: string[] = [];
private format: "json" | "regular";
private stream: NodeJS.WritableStream;
public constructor(options: ConsoleLoggerSinkOptions = {}) {
public constructor(private options: ConsoleLoggerSinkOptions = {}) {
this.stream = options.stream ?? process.stdout;
this.format = options.format ?? "regular";
this.formatter = createLogFormatter(options.format, options);
}
public log(log: LogInfo) {
const line = this.formatter.log(log);
// eslint-disable-next-line no-console
console.log(line);
if (this.currentProgressBar) {
this.pendingLogs.push(line);
} else {
this.writeLine(line);
}
}
public startProgress(initialName?: string): ProgressTracker {
if (this.format === "regular") {
return this.startProgressBar(initialName);
} else {
return NoopProgress;
}
}
private startProgressBar(initialName?: string): ProgressTracker {
if (this.currentProgressBar === undefined) {
this.currentProgressBar = new progressBar.MultiBar(
{
hideCursor: true,
stream: this.stream,
noTTYOutput: this.options.progressNoTTYOutput,
format: "{name} [{bar}] {percentage}% | {value}/{total}",
forceRedraw: true, // without this the bar is flickering,
},
progressBar.Presets.legacy,
);
}
const multiBar = this.currentProgressBar;
multiBar.on("redraw-pre", () => {
if (this.pendingLogs.length > 0) {
if ("clearLine" in this.stream) {
(this.stream as WriteStream).clearLine(1);
}
}
while (this.pendingLogs.length > 0) {
this.writeLine(this.pendingLogs.shift());
}
});
multiBar.on("stop", () => {
this.currentProgressBar = undefined;
while (this.pendingLogs.length > 0) {
this.writeLine(this.pendingLogs.shift());
}
});
let bar: progressBar.SingleBar | undefined;
const update = (progress: Progress) => {
const name = progress.name ?? initialName ?? "progress";
if (bar === undefined) {
bar = multiBar.create(progress.total, 0, { name });
this.bars.push(bar);
} else {
bar.setTotal(progress.total);
}
bar.update(progress.current, { name });
};
const stop = () => {
if (bar) {
bar.update(bar.getTotal());
bar.stop();
multiBar.remove(bar);
this.bars = this.bars.filter((x) => x !== bar);
if (this.bars.length === 0) {
multiBar.stop();
this.currentProgressBar = undefined;
}
}
};
return {
update,
stop,
};
}
private writeLine(line: string | undefined) {
if (line !== undefined) {
this.stream.write(Buffer.from(`${line}\n`));
}
}
}
@ -34,3 +136,8 @@ export class ConsoleLogger extends AutorestSyncLogger {
super({ sinks: [new ConsoleLoggerSink(options)] });
}
}
const NoopProgress = {
update: () => null,
stop: () => null,
};

Просмотреть файл

@ -1,88 +0,0 @@
/* eslint-disable no-console */
import { ConsoleLogger } from ".";
global.console = { log: jest.fn() } as any;
describe("ConsoleLogger", () => {
let logger: ConsoleLogger;
beforeEach(() => {
jest.resetAllMocks();
});
describe("pretty format", () => {
beforeEach(() => {
logger = new ConsoleLogger({
color: false,
timestamp: false,
});
});
it("log debug", () => {
logger.debug("This is some debug");
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("debug | This is some debug");
});
it("log verbose", () => {
logger.verbose("This is some verbose");
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("verbose | This is some verbose");
});
it("log information", () => {
logger.info("This is some information");
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("info | This is some information");
});
it("log warning", () => {
logger.trackWarning({
code: "TestWarning",
message: "This is some warning",
});
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("warning | TestWarning | This is some warning");
});
it("log error", () => {
logger.trackError({
code: "TestError",
message: "This is some error",
});
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("error | TestError | This is some error");
});
it("log fatal", () => {
logger.fatal("This is some fatal error");
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith("fatal | This is some fatal error");
});
});
describe("json format", () => {
beforeEach(() => {
logger = new ConsoleLogger({
format: "json",
timestamp: false,
});
});
it("log debug", () => {
logger.debug("This is some debug");
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith('{"level":"debug","message":"This is some debug"}');
});
it("log warning", () => {
logger.trackWarning({
code: "TestWarning",
message: "This is some warning",
});
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(
'{"level":"warning","code":"TestWarning","message":"This is some warning"}',
);
});
});
});

Просмотреть файл

@ -3,6 +3,7 @@ import { AutorestSyncLogger, FilterLogger, FilterLoggerOptions } from ".";
describe("FilterLogger", () => {
const sink = {
log: jest.fn(),
startProgress: jest.fn(),
};
beforeEach(() => {

Просмотреть файл

@ -8,6 +8,7 @@ describe("Logger", () => {
beforeEach(() => {
sink = {
log: jest.fn(),
startProgress: jest.fn(),
};
});
@ -28,6 +29,7 @@ describe("Logger", () => {
it("sends message to each sink", () => {
const otherSink = {
log: jest.fn(),
startProgress: jest.fn(),
};
const logger = new AutorestSyncLogger({

Просмотреть файл

@ -9,6 +9,7 @@ import {
LoggerSink,
LogInfo,
} from "./types";
import { Progress } from ".";
export interface AutorestLoggerBaseOptions<T> {
processors?: T[];
@ -67,6 +68,27 @@ export abstract class AutorestLoggerBase<T> implements AutorestLogger {
});
}
public startProgress(initialName?: string) {
const sinkProgressTrackers = this.sinks.map((x) => x.startProgress(initialName));
const update = (progress: Progress) => {
for (const tracker of sinkProgressTrackers) {
tracker.update(progress);
}
};
const stop = () => {
for (const tracker of sinkProgressTrackers) {
tracker.stop();
}
};
return {
update,
stop,
};
}
protected emitLog(log: LogInfo) {
for (const sink of this.sinks) {
sink.log(log);

Просмотреть файл

@ -67,6 +67,28 @@ export interface AutorestError extends Omit<AutorestDiagnostic, "level"> {}
export interface AutorestWarning extends Omit<AutorestDiagnostic, "level"> {}
export interface Progress {
/**
* Current step.
*/
current: number;
/**
* Total number of steps.
*/
total: number;
/**
* Optional name
*/
name?: string;
}
export interface ProgressTracker {
update(progress: Progress): void;
stop(): void;
}
/**
* AutorestLogger is an interface for the autorest logger that can be passed around in plugins.
* This can be used to log information, debug logs or track errors and warnings.
@ -91,6 +113,8 @@ export interface IAutorestLogger {
trackWarning(error: AutorestWarning): void;
log(log: LogInfo): void;
startProgress(initialName?: string): ProgressTracker;
}
export interface AutorestLogger extends IAutorestLogger {
@ -119,6 +143,7 @@ export interface LoggerAsyncProcessor {
export interface LoggerSink {
log(info: LogInfo): void;
startProgress(initialName?: string): ProgressTracker;
}
export type EnhancedLogInfo = Omit<LogInfo, "source"> & {

Просмотреть файл

@ -1,7 +1,7 @@
import { AutorestLogger, OperationAbortedException } from "@autorest/common";
import { exists, filePath } from "@azure-tools/async-io";
import { DataStore, IFileSystem, RealFileSystem, CachingFileSystem } from "@azure-tools/datastore";
import { Extension, ExtensionManager, LocalExtension } from "@azure-tools/extension";
import { Extension, ExtensionManager, LocalExtension, PackageInstallProgress } from "@azure-tools/extension";
import { createFileUri, resolveUri, simplifyUri, fileUriToPath } from "@azure-tools/uri";
import { last } from "lodash";
import untildify from "untildify";
@ -45,6 +45,11 @@ export interface ConfigurationLoaderOptions {
extensionManager?: ExtensionManager;
}
/**
* Timeout in ms.
*/
const InstallPackageTimeout = 5 * 60 * 1000;
/**
* Class handling the loading of an autorest configuration.
*/
@ -280,14 +285,29 @@ export class ConfigurationLoader {
} else {
// acquire extension
const pack = await extMgr.findPackage(extensionDef.name, extensionDef.source);
this.logger.info(`> Installing AutoRest extension '${extensionDef.name}' (${extensionDef.source})`);
const extension = await extMgr.installPackage(pack, false, 5 * 60 * 1000, (progressInit: any) =>
progressInit.Message.Subscribe((s: any, m: any) => this.logger.verbose(m)),
);
this.logger.info(
`> Installed AutoRest extension '${extensionDef.name}' (${extensionDef.source}->${extension.version})`,
`> Installing AutoRest extension '${extensionDef.name}' (${extensionDef.source} -> ${pack.version})`,
);
return extension;
const progress = this.logger.startProgress("installing...");
try {
const extension = await extMgr.installPackage(
pack,
false,
InstallPackageTimeout,
(status: PackageInstallProgress) => {
progress.update({ ...status });
},
);
progress.stop();
this.logger.info(
`> Installed AutoRest extension '${extensionDef.name}' (${extensionDef.source}->${extension.version})`,
);
return extension;
} catch (e) {
progress.stop();
throw e;
}
}
}
}

Просмотреть файл

@ -1,5 +1,5 @@
import fs from "fs";
import { fileURLToPath, URL, Url } from "url";
import { LogLevel } from "@autorest/common";
import { AutorestNormalizedConfiguration } from "./autorest-normalized-configuration";
export function isIterable(target: any): target is Iterable<any> {
return !!target && typeof target[Symbol.iterator] === "function";
@ -26,3 +26,7 @@ export function arrayOf<T>(value: T | T[] | undefined): T[] {
}
return [value];
}
export function getLogLevel(config: AutorestNormalizedConfiguration): LogLevel {
return config.debug ? "debug" : config.verbose ? "verbose" : config.level ?? "information";
}

Просмотреть файл

@ -41,7 +41,6 @@
},
"dependencies": {
"@azure-tools/async-io": "~3.0.0",
"@azure-tools/eventing": "~3.0.0",
"@azure-tools/tasks": "~3.0.0",
"@azure/logger": "^1.0.2",
"command-exists": "~1.2.9",

Просмотреть файл

@ -1,4 +1,5 @@
import { Extension } from "./extension";
import { PackageManagerLogEntry } from "./package-manager";
import { SystemRequirementError } from "./system-requirements";
export class UnresolvedPackageException extends Error {

Просмотреть файл

@ -7,7 +7,6 @@ import { ChildProcess, spawn } from "child_process";
import { homedir, tmpdir } from "os";
import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, resolve } from "path";
import { exists, isDirectory, isFile, mkdir, readdir, readFile, rmdir } from "@azure-tools/async-io";
import { Progress, Subscribe } from "@azure-tools/eventing";
import { CriticalSection, Delay, Exception, Mutex, shallowCopy, SharedLock } from "@azure-tools/tasks";
import { resolve as npmResolvePackage } from "npm-package-arg";
import * as pacote from "pacote";
@ -23,7 +22,7 @@ import {
import { Extension, Package } from "./extension";
import { logger } from "./logger";
import { Npm } from "./npm";
import { PackageManager, PackageManagerType } from "./package-manager";
import { PackageManager, PackageManagerLogEntry, PackageManagerProgress, PackageManagerType } from "./package-manager";
import {
patchPythonPath,
PythonCommandLine,
@ -32,6 +31,10 @@ import {
} from "./system-requirements";
import { Yarn } from "./yarn";
export interface PackageInstallProgress extends PackageManagerProgress {
pkg: Package;
}
function quoteIfNecessary(text: string): string {
if (text && text.indexOf(" ") > -1 && text.charAt(0) != '"') {
return `"${text}"`;
@ -321,16 +324,12 @@ export class ExtensionManager {
pkg: Package,
force?: boolean,
maxWait: number = 5 * 60 * 1000,
progressInit: Subscribe = () => {},
reportProgress: (progress: PackageInstallProgress) => void = () => {},
): Promise<Extension> {
if (!this.sharedLock) {
throw new Exception("Extension manager has been disposed.");
}
const progress = new Progress(progressInit);
progress.Start.Dispatch(null);
// will throw if the CriticalSection lock can't be acquired.
// we need this so that only one extension at a time can start installing
// in this process (since to use NPM right, we have to do a change dir before runinng it)
@ -343,12 +342,10 @@ export class ExtensionManager {
const extension = new Extension(pkg, this.installationPath);
const release = await new Mutex(extension.location).acquire(maxWait / 2);
const cwd = process.cwd();
try {
// change directory
process.chdir(this.installationPath);
progress.Progress.Dispatch(25);
if (await isDirectory(extension.location)) {
if (!force) {
@ -359,7 +356,7 @@ export class ExtensionManager {
// force removal first
try {
progress.NotifyMessage(`Removing existing extension ${extension.location}`);
// progress.NotifyMessage(`Removing existing extension ${extension.location}`);
await Delay(100);
await rmdir(extension.location);
} catch (e) {
@ -370,23 +367,26 @@ export class ExtensionManager {
// create the folder
await mkdir(extension.location);
progress.NotifyMessage(`Installing ${pkg.name}, ${pkg.version}`);
const results = this.packageManager.install(extension.location, [pkg.packageMetadata._resolved], { force });
const promise = this.packageManager.install(
extension.location,
[pkg.packageMetadata._resolved],
{ force },
(progress) => {
reportProgress({ pkg, ...progress });
},
);
await extensionRelease();
await results;
progress.NotifyMessage(`Package Install completed ${pkg.name}, ${pkg.version}`);
return extension;
} catch (e: any) {
progress.NotifyMessage(e);
if (e.stack) {
progress.NotifyMessage(e.stack);
const result = await promise;
if (result.success) {
return extension;
} else {
const message = [result.error.message, "", "Installation logs: ", formatLogEntries(result.error.logs)];
throw new PackageInstallationException(pkg.name, pkg.version, message.join("\n"));
}
} catch (e: any) {
// clean up the attempted install directory
if (await isDirectory(extension.location)) {
progress.NotifyMessage(`Cleaning up failed installation: ${extension.location}`);
await Delay(100);
await rmdir(extension.location);
}
@ -394,13 +394,14 @@ export class ExtensionManager {
if (e instanceof Exception) {
throw e;
}
if (e instanceof PackageInstallationException) {
throw e;
}
if (e instanceof Error) {
throw new PackageInstallationException(pkg.name, pkg.version, e.message + e.stack);
}
throw new PackageInstallationException(pkg.name, pkg.version, `${e}`);
} finally {
progress.Progress.Dispatch(100);
progress.End.Dispatch(null);
await Promise.all([extensionRelease(), release()]);
}
}
@ -516,3 +517,14 @@ export class ExtensionManager {
}
}
}
function formatLogEntries(entries: PackageManagerLogEntry[]): string {
const lines = ["```", ...entries.map(formatLogEntry), "```"];
return lines.join("\n");
}
function formatLogEntry(entry: PackageManagerLogEntry): string {
const [first, ...lines] = entry.message.split("\n");
const spacing = " ".repeat(entry.severity.length);
return [`${entry.severity}: ${first}`, ...lines.map((x) => `${spacing} ${x}`)].join("\n");
}

Просмотреть файл

@ -1,6 +1,6 @@
import { dirname, resolve } from "path";
import { execute } from "./exec-cmd";
import { ensurePackageJsonExists, InstallOptions, PackageManager } from "./package-manager";
import { ensurePackageJsonExists, InstallOptions, PackageInstallationResult, PackageManager } from "./package-manager";
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
@ -21,7 +21,11 @@ export const execNpm = async (cwd: string, ...args: string[]) => {
};
export class Npm implements PackageManager {
public async install(directory: string, packages: string[], options?: InstallOptions) {
public async install(
directory: string,
packages: string[],
options?: InstallOptions,
): Promise<PackageInstallationResult> {
await ensurePackageJsonExists(directory);
const output = await execNpm(
@ -34,14 +38,16 @@ export class Npm implements PackageManager {
...packages,
);
if (output.error) {
/* eslint-disable no-console */
console.error("NPM log:");
console.log("-".repeat(50));
console.error(output.log);
console.log("-".repeat(50));
/* eslint-enable no-console */
throw Error(`Failed to install package '${packages}' -- ${output.error}`);
return {
success: false,
error: {
message: `Failed to install package '${packages}' -- ${output.error}`,
logs: output.log.split("\n").map((x) => ({ severity: "info", message: x })),
},
};
}
return { success: true };
}
public async clean(directory: string): Promise<void> {

Просмотреть файл

@ -9,8 +9,49 @@ export interface InstallOptions {
force?: boolean;
}
export interface PackageManagerProgress {
/**
* Current step.
*/
current: number;
/**
* Total number of steps.
*/
total: number;
/**
* In the case there is multiple progress
*/
id?: number;
}
export type PackageInstallationResult = { success: false; error: InstallationError } | { success: true };
export interface InstallationError {
/**
* Main error message.
*/
message: string;
/**
* Log entries for the package manager.
*/
logs: PackageManagerLogEntry[];
}
export interface PackageManagerLogEntry {
severity: "info" | "warning" | "error";
message: string;
}
export interface PackageManager {
install(directory: string, packages: string[], options?: InstallOptions): Promise<void>;
install(
directory: string,
packages: string[],
options?: InstallOptions,
reportProgress?: (progress: PackageManagerProgress) => void,
): Promise<PackageInstallationResult>;
clean(directory: string): Promise<void>;

Просмотреть файл

@ -4,7 +4,14 @@ import { join, resolve } from "path";
import { isFile, writeFile } from "@azure-tools/async-io";
import { execute } from "./exec-cmd";
import { DEFAULT_NPM_REGISTRY } from "./npm";
import { ensurePackageJsonExists, InstallOptions, PackageManager } from "./package-manager";
import {
ensurePackageJsonExists,
InstallOptions,
PackageInstallationResult,
PackageManager,
PackageManagerLogEntry,
PackageManagerProgress,
} from "./package-manager";
let _cli: string | undefined;
const getPathToYarnCli = async () => {
@ -35,37 +42,68 @@ const getPathToYarnCli = async () => {
export class Yarn implements PackageManager {
public constructor(private pathToYarnCli: string | undefined = undefined) {}
public async install(directory: string, packages: string[], options?: InstallOptions) {
public async install(
directory: string,
packages: string[],
options?: InstallOptions,
reportProgress?: (progress: PackageManagerProgress) => void,
): Promise<PackageInstallationResult> {
await ensurePackageJsonExists(directory);
let total = 1;
const logs: PackageManagerLogEntry[] = [];
const handleYarnEvent = (event: YarnEvent) => {
switch (event.type) {
case "progressStart":
if (event.data.total !== 0) {
reportProgress?.({ current: 0, total, id: event.data.id });
total = event.data.total;
}
break;
case "progressFinish":
reportProgress?.({ current: 100, total, id: event.data.id });
break;
case "progressTick":
reportProgress?.({ current: Math.min(event.data.current, total), total, id: event.data.id });
break;
case "error":
case "info":
case "warning":
logs.push({ severity: event.type, message: event.data });
break;
case "step":
logs.push({ severity: "info", message: ` Step: ${event.data.message}` });
break;
}
};
const output = await this.execYarn(
directory,
"add",
"--global-folder",
directory.replace(/\\/g, "/"),
...(options?.force ? ["--force"] : []),
...packages,
["add", "--global-folder", directory.replace(/\\/g, "/"), ...(options?.force ? ["--force"] : []), ...packages],
handleYarnEvent,
);
if (output.error) {
/* eslint-disable no-console */
console.error("Yarn log:");
console.log("-".repeat(50));
console.error(output.log);
console.log("-".repeat(50));
/* eslint-enable no-console */
throw Error(`Failed to install package '${packages}' -- ${output.error}`);
return {
success: false,
error: {
message: `Failed to install package '${packages}' -- ${output.error}`,
logs,
},
};
} else {
return { success: true };
}
}
public async clean(directory: string): Promise<void> {
await this.execYarn(directory, "cache", "clean", "--force");
await this.execYarn(directory, ["cache", "clean", "--force"]);
}
public async getPackageVersions(directory: string, packageName: string): Promise<string[]> {
const result = await this.execYarn(directory, "info", packageName, "versions", "--json");
const result = await this.execYarn(directory, ["info", packageName, "versions", "--json"]);
return JSON.parse(result.stdout).data;
}
public async execYarn(cwd: string, ...args: string[]) {
public async execYarn(cwd: string, args: string[], onYarnEvent?: (event: YarnEvent) => void) {
const procArgs = [
this.pathToYarnCli ?? (await getPathToYarnCli()),
"--no-node-version-check",
@ -81,6 +119,47 @@ export class Yarn implements PackageManager {
YARN_IGNORE_PATH: "1", // Prevent yarn from using a different version if configured in ~/.yarnrc
};
return await execute(process.execPath, procArgs, { cwd, env: newEnv });
const handleYarnLog = (buffer: Buffer) => {
const str = buffer.toString();
for (const line of str.split(/\r?\n/).filter((x) => x !== "")) {
try {
const data = JSON.parse(line);
onYarnEvent?.(data);
} catch (e) {
// NOOP
}
}
};
return await execute(process.execPath, procArgs, {
cwd,
env: newEnv,
onStdOutData: handleYarnLog,
onStdErrData: handleYarnLog,
});
}
}
type YarnEvent = YarnProgressTick | YarnProgressStart | YarnProgressFinish | YarnStep | YarnLog;
interface YarnProgressTick {
type: "progressTick";
data: { id: number; current: number };
}
interface YarnProgressStart {
type: "progressStart";
data: { id: number; total: number };
}
interface YarnProgressFinish {
type: "progressFinish";
data: { id: number };
}
interface YarnStep {
type: "step";
data: { message: string; current: number; total: number };
}
interface YarnLog {
type: "info" | "warning" | "error";
data: string;
}

Просмотреть файл

@ -1,9 +1,9 @@
/* eslint-disable no-console */
import * as asyncio from "@azure-tools/async-io";
import * as tasks from "@azure-tools/tasks";
import assert from "assert";
import * as fs from "fs";
import * as os from "os";
import * as asyncio from "@azure-tools/async-io";
import * as tasks from "@azure-tools/tasks";
import { ExtensionManager, InvalidPackageIdentityException, UnresolvedPackageException } from "../src";
const tmpFolder = fs.mkdtempSync(`${fs.mkdtempSync(`${os.tmpdir()}/test`)}/install-pkg`);
@ -41,11 +41,7 @@ describe("TestExtensions", () => {
console.log("Installing Once");
// install it once
const dni = await extensionManager.findPackage("echo-cli", "*");
const installing = extensionManager.installPackage(dni, false, 60000, (i) =>
i.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
}),
);
const installing = extensionManager.installPackage(dni, false, 60000, (i) => {});
const extension = await installing;
assert.notEqual(await extension.configuration, "the configuration file isnt where it should be?");
}
@ -54,19 +50,11 @@ describe("TestExtensions", () => {
console.log("Attempt Overwrite");
// install/overwrite
const dni = await extensionManager.findPackage("echo-cli", "*");
const installing = extensionManager.installPackage(dni, true, 60000, (i) =>
i.Message.Subscribe((s, m) => {
console.log(`Installer2:${m}`);
}),
);
const installing = extensionManager.installPackage(dni, true, 60000, (i) => {});
// install at the same time?
const dni2 = await extensionManager.findPackage("echo-cli", "*");
const installing2 = extensionManager.installPackage(dni2, true, 60000, (i) =>
i.Message.Subscribe((s, m) => {
console.log(`Installer3:${m}`);
}),
);
const installing2 = extensionManager.installPackage(dni2, true, 60000, (i) => {});
// wait for it.
const extension = await installing;
@ -149,11 +137,7 @@ describe("TestExtensions", () => {
"Install Extension",
async () => {
const dni = await extensionManager.findPackage("echo-cli", "1.0.8");
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {
installing.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
});
});
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {});
const extension = await installing;
@ -176,11 +160,7 @@ describe("TestExtensions", () => {
"Install Extension via star",
async () => {
const dni = await extensionManager.findPackage("echo-cli", "*");
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {
installing.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
});
});
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {});
const extension = await installing;
assert.notEqual(await extension.configuration, "");
@ -202,11 +182,7 @@ describe("TestExtensions", () => {
"Force install",
async () => {
const dni = await extensionManager.findPackage("echo-cli", "*");
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {
installing.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
});
});
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {});
const extension = await installing;
assert.notEqual(await extension.configuration, "");
@ -214,11 +190,7 @@ describe("TestExtensions", () => {
await asyncio.rmFile(await extension.configurationPath);
// reinstall with force!
const installing2 = extensionManager.installPackage(dni, true, 5 * 60 * 1000, (installing) => {
installing.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
});
});
const installing2 = extensionManager.installPackage(dni, true, 5 * 60 * 1000, (installing) => {});
const extension2 = await installing2;
// is the file back?
@ -232,11 +204,7 @@ describe("TestExtensions", () => {
async () => {
try {
const dni = await extensionManager.findPackage("none", "fearthecowboy/echo-cli");
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {
installing.Message.Subscribe((s, m) => {
console.log(`Installer:${m}`);
});
});
const installing = extensionManager.installPackage(dni, false, 5 * 60 * 1000, (installing) => {});
const extension = await installing;
assert.notEqual(await extension.configuration, "");
const proc = await extension.start();

Просмотреть файл

@ -17,6 +17,10 @@ export function createMockLogger(overrides: Partial<AutorestLogger> = {}): Autor
trackError: jest.fn(),
log: jest.fn(),
with: jest.fn(() => logger),
startProgress: jest.fn(() => ({
update: jest.fn(),
stop: jest.fn(),
})),
diagnostics: [],
...overrides,
};
@ -35,7 +39,17 @@ export class AutorestTestLogger extends AutorestLoggerBase<LoggerProcessor> {
};
public constructor() {
super({ sinks: [{ log: (x) => this.log(x) }] });
super({
sinks: [
{
log: (x) => this.log(x),
startProgress: jest.fn(() => ({
update: jest.fn(),
stop: jest.fn(),
})),
},
],
});
}
public log(log: LogInfo): void {