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 { CriticalSection , Delay , Exception , Mutex , shallowCopy , SharedLock } from "@azure-tools/tasks" ; import { resolve as npmResolvePackage } from "npm-package-arg" ; import * as pacote from "pacote" ; import * as semver from "semver" ; import { UnresolvedPackageException , InvalidPackageIdentityException , ExtensionFolderLocked , PackageInstallationException , MissingStartCommandException , UnsatisfiedSystemRequirementException , } from "./exceptions" ; import { Extension , Package } from "./extension" ; import { logger } from "./logger" ; import { Npm } from "./npm" ; import { PackageManager , PackageManagerLogEntry , PackageManagerProgress , PackageManagerType } from "./package-manager" ; import { patchPythonPath , PythonCommandLine , ExtensionSystemRequirements , validateExtensionSystemRequirements , } from "./system-requirements" ; import { Yarn } from "./yarn" ; export interface PackageInstallProgress extends PackageManagerProgress { pkg : Package ; } function quoteIfNecessary ( text ) { if ( text && text . indexOf ( " " ) > - 1 && text . charAt ( 0 ) != '"' ) { return `"${ text }"` ; } return text ; } const nodePath = quoteIfNecessary ( process . execPath ) ; function cmdlineToArray ( text , result = [ ] , matcher = /[^\s"]+|"([^"]*)"/gi , count = 0 ) { text = text . replace ( /\\"/g , "\ufffe" ) ; const match = matcher . exec ( text ) ; return match ? cmdlineToArray ( text , result , matcher , result . push ( match [ 1 ] ? match [ 1 ] . replace ( /\ufffe/g , '\\"' ) : match [ 0 ] . replace ( /\ufffe/g , '\\"' ) ) ) : result ; } function getPathVariableName ( ) { if ( process . platform === "win32" ) { let PATH = "Path" ; Object . keys ( process . env ) . forEach ( function ( e ) { if ( e . match ( /^PATH$/i ) ) { PATH = e ; } } ) ; return PATH ; } return "PATH" ; } async function realPathWithExtension ( command ) { const pathExt = ( process . env . pathext || ".EXE" ) . split ( ";" ) ; for ( const each of pathExt ) { const filename = `${ command }${ each }` ; if ( await isFile ( filename ) ) { return filename ; } } return undefined ; } async function getFullPath ( command , searchPath ? ) { command = command . replace ( /"/g , "" ) ; const ext = extname ( command ) ; if ( isAbsolute ( command ) ) { if ( ext || process . platform !== "win32" ) { if ( await isFile ( command ) ) { return command ; } } if ( process . platform === "win32" ) { const cmd = await realPathWithExtension ( command ) ; if ( cmd ) { return cmd ; } } return undefined ; } if ( searchPath ) { const folders = searchPath . split ( delimiter ) ; for ( const each of folders ) { const fullPath = await getFullPath ( resolve ( each , command ) ) ; if ( fullPath ) { return fullPath ; } } } return undefined ; } param spec returns export async function fetchPackageMetadata ( spec ) { try { return await pacote . manifest ( spec , { cache : `${ tmpdir ( ) }/cache` , registry : process . env . autorest_registry || "https://registry.npmjs.org" , "full-metadata" : true , } ) ; } catch ( error ) { logger . error ( `Error resolving package ${ spec }` , error ) ; throw new UnresolvedPackageException ( spec ) ; } } function resolveName ( name , version ) { try { return npmResolvePackage ( name , version ) ; } catch ( e ) { if ( e instanceof Error ) { throw new InvalidPackageIdentityException ( name , version , e . message ) ; } else { throw e ; } } } export class ExtensionManager { public dotnetPath = normalize ( `${ homedir ( ) }/.dotnet` ) ; public static async Create ( installationPath , packageManagerType = "yarn" , packageManagerPath = undefined ) { if ( ! ( await exists ( installationPath ) ) ) { await mkdir ( installationPath ) ; } if ( ! ( await isDirectory ( installationPath ) ) ) { throw new Exception ( `Extension folder '${ installationPath }' is not a valid directory` ) ; } const lock = new SharedLock ( installationPath ) ; const packageManager = packageManagerType === "yarn" ? new Yarn ( packageManagerPath ) : new Npm ( ) ; return new ExtensionManager ( installationPath , lock , await lock . acquire ( ) , packageManager ) ; } public async dispose ( ) { await this . disposeLock ( ) ; this . disposeLock = async ( ) => { } ; this . sharedLock = null ; } public async reset ( ) { if ( ! this . sharedLock ) { throw new Exception ( "Extension manager has been disposed." ) ; } const release = await this . sharedLock . exclusive ( ) ; try { await rmdir ( this . installationPath ) ; await mkdir ( this . installationPath ) ; await this . packageManager . clean ( this . installationPath ) ; } catch ( e ) { throw new ExtensionFolderLocked ( this . installationPath ) ; } finally { await release ( ) ; } } private constructor ( private installationPath , private sharedLock , private disposeLock , private packageManager ) { } public async getPackageVersions ( name ) { const versions = await this . packageManager . getPackageVersions ( process . cwd ( ) , name ) ; return versions . sort ( ( b , a ) => semver . compare ( a , b ) ) ; } public async findPackage ( name , version = "latest" ) { if ( version . endsWith ( ".tgz" ) ) { const pm = await fetchPackageMetadata ( version ) ; return new Package ( pm , pm , this ) ; } try { const resolved = resolveName ( name , version ) ; let resolvedName = resolved . raw ; if ( version . startsWith ( "~" ) || version . startsWith ( "^" ) ) { const vers = ( await this . getPackageVersions ( resolved . raw ) ) . filter ( ( each ) => semver . satisfies ( each , version ) ) ; if ( vers . length > 0 ) { resolvedName = `${ resolved . name }@${ vers [ 0 ] }` ; } } const pm = await fetchPackageMetadata ( resolvedName ) ; return new Package ( resolved , pm , this ) ; } catch ( E ) { if ( name . startsWith ( "@autorest/" ) ) { const githubRepo = name . replace ( "@autorest/" , "autorest." ) ; const githubPkgName = name . replace ( "@" , "" ) . replace ( "autorest/" , "autorest-" ) ; const githubVersion = version . replace ( /^[~|^]/g , "" ) . replace ( /_/g , "-" ) ; const ghurl = `https://github.com/Azure/${ githubRepo }/releases/download/v${ githubVersion }/${ githubPkgName }-${ githubVersion }.tgz` ; try { const pm = await fetchPackageMetadata ( ghurl ) ; if ( pm ) { return new Package ( pm , pm , this ) ; } } catch { } } throw E ; } } public async getInstalledExtension ( name , version ) { if ( ! semver . validRange ( version ) ) { const pkg = await this . findPackage ( name , version ) ; version = pkg . version ; } const installed = await this . getInstalledExtensions ( ) ; for ( const each of installed ) { if ( name === each . name && semver . satisfies ( each . version , version ) ) { return each ; } } return null ; } public async getInstalledExtensions ( ) { const results = new Array < Extension > ( ) ; for ( const folder of await readdir ( this . installationPath ) ) { const fullpath = join ( this . installationPath , folder ) ; if ( await isDirectory ( fullpath ) ) { const split = /((@.+)_)?(.+)@(.+)/ . exec ( folder ) ; if ( split ) { try { const org = split [ 2 ] ; const name = split [ 3 ] ; const version = split [ 4 ] ; const actualPath = org ? normalize ( `${ fullpath }/node_modules/${ org }/${ name }` ) : normalize ( `${ fullpath }/node_modules/${ name }` ) ; const pm = await fetchPackageMetadata ( actualPath ) ; const ext = new Extension ( new Package ( null , pm , this ) , this . installationPath ) ; if ( fullpath !== ext . location ) { console . trace ( `WARNING: Not reporting '${ fullpath }' since its package.json claims it should be at '${ ext . location }' (probably symlinked once and modified later)` ) ; continue ; } results . push ( ext ) ; } catch ( e ) { } } } } return results ; } private static criticalSection = new CriticalSection ( ) ; public async installPackage ( pkg , force ? , maxWait = 5 * 60 * 1000 , reportProgress = ( ) => { } ) { if ( ! this . sharedLock ) { throw new Exception ( "Extension manager has been disposed." ) ; } const extensionRelease = await ExtensionManager . criticalSection . acquire ( maxWait ) ; if ( ! ( await exists ( this . installationPath ) ) ) { await mkdir ( this . installationPath ) ; } const extension = new Extension ( pkg , this . installationPath ) ; const release = await new Mutex ( extension . location ) . acquire ( maxWait / 2 ) ; try { process . chdir ( this . installationPath ) ; if ( await isDirectory ( extension . location ) ) { if ( ! force ) { return extension ; } try { await Delay ( 100 ) ; await rmdir ( extension . location ) ; } catch ( e ) { } } await mkdir ( extension . location ) ; const promise = this . packageManager . install ( extension . location , [ pkg . packageMetadata . _resolved ] , { force } , ( progress ) => { reportProgress ( { pkg , ... progress } ) ; } ) ; await extensionRelease ( ) ; 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 ) { if ( await isDirectory ( extension . location ) ) { await Delay ( 100 ) ; await rmdir ( extension . location ) ; } 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 { await Promise . all ( [ extensionRelease ( ) , release ( ) ] ) ; } } public async removeExtension ( extension ) { if ( await isDirectory ( extension . location ) ) { const release = await new Mutex ( extension . location ) . acquire ( ) ; await rmdir ( extension . location ) ; await release ( ) ; } } public async start ( extension , enableDebugger = false ) { const PathVar = getPathVariableName ( ) ; await this . validateExtensionSystemRequirements ( extension ) ; if ( ! extension . definition . scripts ) { throw new MissingStartCommandException ( extension ) ; } const script = enableDebugger && extension . definition . scripts . debug ? extension . definition . scripts . debug : extension . definition . scripts . start ; if ( ! script ) { throw new MissingStartCommandException ( extension ) ; } const command = cmdlineToArray ( script ) ; if ( command . length === 0 ) { throw new MissingStartCommandException ( extension ) ; } const env = shallowCopy ( process . env ) ; env [ PathVar ] = `${ join ( extension . modulePath , "node_modules" , ".bin" ) }${ delimiter }${ env [ PathVar ] }` ; env [ PathVar ] = `${ join ( extension . location , "node_modules" , ".bin" ) }${ delimiter }${ env [ PathVar ] }` ; switch ( command [ 0 ] . toLowerCase ( ) ) { case "node" : case "node.exe" : command [ 0 ] = nodePath ; break ; case "python" : case "python.exe" : case "python3" : case "python3.exe" : await patchPythonPath ( command as PythonCommandLine , { version : ">=3.6" } ) ; break ; } for ( let i = 0 ; i < command . length ; i ++ ) { command [ i ] = quoteIfNecessary ( command [ i ] ) ; } const fullCommandPath = await getFullPath ( command [ 0 ] , env [ getPathVariableName ( ) ] ) ; if ( ! fullCommandPath ) { throw new Exception ( `Unable to resolve full path for executable '${ command [ 0 ] }' -- (cmdline '${ command . join ( " " ) }')` ) ; } if ( process . platform === "win32" && fullCommandPath . indexOf ( " " ) > - 1 && ! /.exe$/gi . exec ( fullCommandPath ) ) { const originalPath = process . env [ PathVar ] ; try { process . env [ PathVar ] = `${ dirname ( fullCommandPath ) }${ delimiter }${ env [ PathVar ] }` ; return spawn ( basename ( fullCommandPath ) , command . slice ( 1 ) , { env , cwd : extension . modulePath , stdio : [ "pipe" , "pipe" , "pipe" ] , } ) ; } finally { process . env [ PathVar ] = originalPath ; } } return spawn ( fullCommandPath , command . slice ( 1 ) , { env , cwd : extension . modulePath , stdio : [ "pipe" , "pipe" , "pipe" ] , } ) ; } param extension private async validateExtensionSystemRequirements ( extension ) { const systemRequirements = extension . definition . systemRequirements ; if ( ! systemRequirements ) { return ; } const errors = await validateExtensionSystemRequirements ( systemRequirements ) ; if ( errors . length > 0 ) { throw new UnsatisfiedSystemRequirementException ( extension , errors ) ; } } } function formatLogEntries ( entries ) { const lines = [ "```" , ... entries . map ( formatLogEntry ) , "```" ] ; return lines . join ( "\n" ) ; } function formatLogEntry ( entry ) { const [ first , ... lines ] = entry . message . split ( "\n" ) ; const spacing = " " . repeat ( entry . severity . length ) ; return [ `${ entry . severity }: ${ first }` , ... lines . map ( ( x ) => `${ spacing }  ${ x }` ) ] . join ( "\n" ) ; }