import * as os from "os" ; import * as vscode from "vscode" ; import * as fs from "fs-plus" ; import * as path from "path" ; import request = require ( "request-promise" ) ; import * as utils from "../utils" ; import { FileNames , ConfigKey , ScaffoldType , OSPlatform } from "../constants" ; import { TelemetryContext } from "../telemetry" ; import { DigitalTwinConstants } from "./DigitalTwinConstants" ; import { CodeGenProjectType , DeviceConnectionType , CodeGenLanguage , DeviceSdkReferenceType , CodeGenExecutionItem } from "./DigitalTwinCodeGen/Interfaces/CodeGenerator" ; import { AnsiCCodeGeneratorFactory } from "./DigitalTwinCodeGen/AnsiCCodeGeneratorFactory" ; import { ConfigHandler } from "../configHandler" ; import { PnpDeviceConnection , CodeGenProjectTemplate , DeviceSdkReference } from "../Models/Interfaces/ProjectTemplate" ; import { DialogResponses } from "../DialogResponses" ; import { RemoteExtension } from "../Models/RemoteExtension" ; import { DigitalTwinUtility } from "./DigitalTwinUtility" ; import { CodeGenUtility } from "./DigitalTwinCodeGen/CodeGenUtility" ; import { FileUtility } from "../FileUtility" ; import { OperationCanceledError } from "../common/Error/OperationCanceledError" ; import { Utility } from "./pnp/src/common/utility" ; import { OperationFailedError } from "../common/Error/OperationFailedErrors/OperationFailedError" ; import { SystemResourceNotFoundError } from "../common/Error/SystemErrors/SystemResourceNotFoundError" ; import { TypeNotSupportedError } from "../common/Error/SystemErrors/TypeNotSupportedError" ; import { WorkspaceNotOpenError } from "../common/Error/OperationFailedErrors/WorkspaceNotOpenError" ; interface CodeGeneratorDownloadLocation { win32Md5 : string ; win32PackageUrl : string ; macOSMd5 : string ; macOSPackageUrl : string ; ubuntuMd5 : string ; ubuntuPackageUrl : string ; } interface CodeGeneratorConfigItem { codeGeneratorVersion : string ; iotWorkbenchMinimalVersion : string ; codeGeneratorLocation : CodeGeneratorDownloadLocation ; } interface CodeGeneratorConfig { codeGeneratorConfigItems : CodeGeneratorConfigItem [ ] ; } interface CodeGenExecutions { codeGenExecutionItems : CodeGenExecutionItem [ ] ; } enum CodeGenCliOperation { None = 0 , Install , Upgrade } enum ReGenResult { Succeeded , Skipped } function compareVersion ( verion1 , verion2 ) { const ver1 = verion1 . split ( "." ) ; const ver2 = verion2 . split ( "." ) ; let i = 0 ; let v1 , v2 ; while ( i < 3 ) { v1 = Number ( ver1 [ i ] ) ; v2 = Number ( ver2 [ i ] ) ; if ( v1 > v2 ) return 1 ; if ( v1 < v2 ) return - 1 ; i ++ ; } return 0 ; } function localCodeGenCliPath ( ) { return path . join ( os . homedir ( ) , DigitalTwinConstants . codeGenCliFolder ) ; } export class CodeGeneratorCore { async generateDeviceCodeStub ( context , channel , telemetryContext ) { RemoteExtension . ensureLocalBeforeRunCommand ( "generate device code stub" , context ) ; const rootPath = utils . getProjectDeviceRootPath ( ) ; if ( ! rootPath ) { throw new WorkspaceNotOpenError ( "generate device code stub" ) ; } if ( ! ( await this . installOrUpgradeCodeGenCli ( context , channel ) ) ) { return ; } const capabilityModelFilePath = await DigitalTwinUtility . selectCapabilityModel ( ) ; const regenResult = await this . regenCode ( rootPath , capabilityModelFilePath , context , channel , telemetryContext ) ; if ( regenResult === ReGenResult . Succeeded ) { return ; } const codeGenProjectName = await this . getCodeGenProjectName ( channel , rootPath ) ; const codeGenLanguage = await this . selectLanguage ( channel ) ; const codeGenOptions = this . readCodeGenOptionsConfiguration ( context ) ; const connectionType = await this . selectConnectionType ( channel , codeGenOptions ) ; const codeGenProjectType = await this . selectProjectTemplate ( channel , codeGenLanguage , codeGenOptions ) ; const sdkReferenceType = await this . selectDeviceSdkReferenceType ( channel , codeGenProjectType , codeGenOptions ) ; if ( ! ( await DigitalTwinUtility . downloadDependentInterface ( rootPath , capabilityModelFilePath ) ) ) { throw new OperationFailedError ( "download dependent interface" , "" , "Check out error message in the output channel." ) ; } const codeGenExecutionInfo = { outputDirectory : path . join ( rootPath , codeGenProjectName ) , capabilityModelFilePath , interfaceDirecoty : rootPath , projectName : codeGenProjectName , languageLabel : CodeGenLanguage . ANSIC , codeGenProjectType , deviceSdkReferenceType : sdkReferenceType , deviceConnectionType : connectionType } ; const scaffoldType = ScaffoldType . Local ; await this . saveCodeGenConfig ( scaffoldType , rootPath , capabilityModelFilePath , codeGenExecutionInfo ) ; await this . generateDeviceCodeCore ( codeGenExecutionInfo , context , channel , telemetryContext ) ; } private async regenCode ( rootPath , capabilityModelFilePath , context , channel , telemetryContext ) { const codeGenConfigPath = path . join ( rootPath , FileNames . vscodeSettingsFolderName , DigitalTwinConstants . codeGenConfigFileName ) ; let codeGenExecutionItem ; if ( ! fs . existsSync ( codeGenConfigPath ) ) { return ReGenResult . Skipped ; } try { const codeGenExecutions = JSON . parse ( fs . readFileSync ( codeGenConfigPath , "utf8" ) ) ; if ( codeGenExecutions ) { codeGenExecutionItem = codeGenExecutions . codeGenExecutionItems . find ( item => item . capabilityModelFilePath === capabilityModelFilePath ) ; } } catch { } if ( codeGenExecutionItem ) { const regenOptions = [ ] ; regenOptions . push ( { label : `Re-generate code for ${ codeGenExecutionItem . projectName }` , description : "" } , { label : "Create new project" , description : "" } ) ; const regenSelection = await vscode . window . showQuickPick ( regenOptions , { ignoreFocusOut : true , placeHolder : "Please select an option:" } ) ; if ( ! regenSelection ) { throw new OperationCanceledError ( "Re-generate code selection cancelled." ) ; } if ( regenSelection . label !== "Create new project" ) { if ( ! ( await DigitalTwinUtility . downloadDependentInterface ( rootPath , capabilityModelFilePath ) ) ) { return ReGenResult . Skipped ; } utils . channelShowAndAppendLine ( channel , `${ DigitalTwinConstants . dtPrefix } Regenerate device code using an existing CodeGen configure:` ) ; CodeGenUtility . printCodeGenConfig ( codeGenExecutionItem , channel ) ; await this . generateDeviceCodeCore ( codeGenExecutionItem , context , channel , telemetryContext ) ; return ReGenResult . Succeeded ; } else { return ReGenResult . Skipped ; } } else { return ReGenResult . Skipped ; } } private async getCodeGenProjectName ( channel , rootPath ) { const codeGenProjectName = await vscode . window . showInputBox ( { placeHolder : "Please input the project name here." , ignoreFocusOut : true , validateInput : ( projectName ) => { if ( ! projectName || projectName . length === 0 ) { return `The project name can't be empty.` ; } if ( ! DigitalTwinConstants . codegenProjectNameRegex . test ( projectName ) ) { return `Project name can only contain ${ DigitalTwinConstants . codegenProjectNameRegexDescription }.` ; } return ; } } ) ; if ( ! codeGenProjectName ) { throw new OperationCanceledError ( `Project name is not specified, cancelled.` ) ; } const projectPath = path . join ( rootPath , codeGenProjectName ) ; if ( fs . isDirectorySync ( projectPath ) ) { const messge = `The folder ${ projectPath } already exists. Do you want to overwrite the contents in this folder?` ; const choice = await vscode . window . showWarningMessage ( messge , DialogResponses . yes , DialogResponses . no ) ; if ( choice !== DialogResponses . yes ) { throw new OperationCanceledError ( `Valid project name is not specified, cancelled.` ) ; } } utils . channelShowAndAppendLine ( channel , `Input project name: ${ codeGenProjectName }` ) ; return codeGenProjectName ; } private readCodeGenOptionsConfiguration ( context ) { const codeGenConfigFilePath = context . asAbsolutePath ( path . join ( FileNames . resourcesFolderName , FileNames . templatesFolderName , FileNames . codeGenOptionsFileName ) ) ; return JSON . parse ( fs . readFileSync ( codeGenConfigFilePath , "utf8" ) ) ; } private async selectLanguage ( channel ) { const languageItems = [ ] ; languageItems . push ( { label : CodeGenLanguage . ANSIC , description : "" } ) ; const languageSelection = await vscode . window . showQuickPick ( languageItems , { ignoreFocusOut : true , placeHolder : "Select the language for generated code:" } ) ; if ( ! languageSelection ) { throw new OperationCanceledError ( "CodeGen language selection cancelled." ) ; } utils . channelShowAndAppendLine ( channel , `Selected CodeGen language: ${ languageSelection . label }` ) ; return languageSelection . label ; } private async selectConnectionType ( channel , codegenOptionsConfig ) { const connectionTypeItems = [ ] ; codegenOptionsConfig . connectionTypes . forEach ( ( element ) => { connectionTypeItems . push ( { label : element . name , detail : element . detail } ) ; } ) ; const deviceConnectionSelection = await vscode . window . showQuickPick ( connectionTypeItems , { ignoreFocusOut : true , placeHolder : "How will device connect to Azure IoT?" } ) ; if ( ! deviceConnectionSelection ) { throw new OperationCanceledError ( "Connection type selection cancelled." ) ; } const deviceConnection = codegenOptionsConfig . connectionTypes . find ( ( connectionType ) => { return connectionType . name === deviceConnectionSelection . label ; } ) ; if ( ! deviceConnection ) { throw new SystemResourceNotFoundError ( "device connection type" , `${ deviceConnectionSelection . label } connection type` , "CodeGen configuration" ) ; } const connectionType = DeviceConnectionType [ deviceConnection . type as keyof typeof DeviceConnectionType ] ; if ( ! connectionType ) { throw new TypeNotSupportedError ( "device connection type" , `${ deviceConnection . type }` ) ; } utils . channelShowAndAppendLine ( channel , `Selected device connection type: ${ connectionType }` ) ; return connectionType ; } private async selectProjectTemplate ( channel , language , codegenOptionsConfig ) { const projectTemplates = codegenOptionsConfig . projectTemplates . filter ( ( projectTemplate ) => { return projectTemplate . enabled && projectTemplate . language === language ; } ) ; const projectTemplateItems = [ ] ; projectTemplates . forEach ( ( element ) => { projectTemplateItems . push ( { label : element . name , detail : element . detail } ) ; } ) ; if ( ! projectTemplateItems ) { throw new SystemResourceNotFoundError ( "project template" , `${ language } language` , "CodeGen configuration" ) ; } const projectTemplateSelection = await vscode . window . showQuickPick ( projectTemplateItems , { ignoreFocusOut : true , placeHolder : "Select project template:" } ) ; if ( ! projectTemplateSelection ) { throw new OperationCanceledError ( "CodeGen project template selection cancelled." ) ; } const projectTemplate = projectTemplates . find ( ( projectType ) => { return projectType . name === projectTemplateSelection . label ; } ) ; if ( ! projectTemplate ) { throw new SystemResourceNotFoundError ( "project template" , `${ projectTemplateSelection . label } project template name` , "CodeGen configuration" ) ; } const codeGenProjectType = CodeGenProjectType [ projectTemplate . type as keyof typeof CodeGenProjectType ] ; if ( ! codeGenProjectType ) { throw new TypeNotSupportedError ( "CodeGen project type" , `${ projectTemplate . type }` ) ; } utils . channelShowAndAppendLine ( channel , `Selected CodeGen project type: ${ codeGenProjectType }` ) ; return codeGenProjectType ; } private async selectDeviceSdkReferenceType ( channel , projectType , codegenOptionsConfig ) { let deviceSdkReferenceType = undefined ; switch ( projectType ) { case CodeGenProjectType . IoTDevKit : deviceSdkReferenceType = DeviceSdkReferenceType . DevKitSDK ; break ; case CodeGenProjectType . CMakeWindows : case CodeGenProjectType . CMakeLinux : { const sdkReferenceTypeItems = [ ] ; codegenOptionsConfig . deviceSdkReferenceTypes . forEach ( ( element ) => { sdkReferenceTypeItems . push ( { label : element . name , detail : element . detail } ) ; } ) ; const deviceConnectionSelection = await vscode . window . showQuickPick ( sdkReferenceTypeItems , { ignoreFocusOut : true , placeHolder : "How will CMake include the Azure IoT Device SDK?" } ) ; if ( ! deviceConnectionSelection ) { throw new OperationCanceledError ( "IoT Device SDK reference type selection cancelled." ) ; } const sdkReference = codegenOptionsConfig . deviceSdkReferenceTypes . find ( ( sdkReference ) => { return sdkReference . name === deviceConnectionSelection . label ; } ) ; if ( ! sdkReference ) { throw new SystemResourceNotFoundError ( "SDK reference" , `${ deviceConnectionSelection . label } IoT Device SDK reference type` , "CodeGen configuration" ) ; } const sdkReferenceType = DeviceSdkReferenceType [ sdkReference . type as keyof typeof DeviceSdkReferenceType ] ; if ( ! sdkReferenceType ) { throw new TypeNotSupportedError ( "SDK reference type" , `${ sdkReference . type }` ) ; } deviceSdkReferenceType = sdkReferenceType ; break ; } default : throw new TypeNotSupportedError ( "project type" , `${ projectType }` ) ; } utils . channelShowAndAppendLine ( channel , `Selected device SDK reference type: ${ deviceSdkReferenceType }` ) ; return deviceSdkReferenceType ; } private async generateDeviceCodeCore ( codeGenExecutionInfo , context , channel , telemetryContext ) { const codeGenFactory = new AnsiCCodeGeneratorFactory ( context , channel , telemetryContext ) ; const codeGenerator = codeGenFactory . createCodeGeneratorImpl ( codeGenExecutionInfo . languageLabel ) ; if ( ! codeGenerator ) { return false ; } const capabilityModel = await Utility . getJsonContent ( codeGenExecutionInfo . capabilityModelFilePath ) ; const capabilityModelId = capabilityModel [ "@id" ] ; await vscode . window . withProgress ( { location : vscode . ProgressLocation . Notification , title : `Generate PnP device code for ${ capabilityModelId } ...` } , async ( ) => { const result = await codeGenerator . generateCode ( codeGenExecutionInfo ) ; if ( result ) { vscode . window . showInformationMessage ( `Generate PnP device code for ${ capabilityModelId } completed` ) ; } } ) ; return true ; } private async saveCodeGenConfig ( type , rootPath , capabilityModelFilePath , codeGenExecutionInfo ) { const codeGenConfigPath = path . join ( rootPath , FileNames . vscodeSettingsFolderName , DigitalTwinConstants . codeGenConfigFileName ) ; try { let codeGenExecutions ; if ( await FileUtility . fileExists ( type , codeGenConfigPath ) ) { const codeGenConfig = await FileUtility . readFile ( type , codeGenConfigPath , "utf8" ) ; codeGenExecutions = JSON . parse ( codeGenConfig as string ) ; if ( codeGenExecutions ) { codeGenExecutions . codeGenExecutionItems = codeGenExecutions . codeGenExecutionItems . filter ( item => item . capabilityModelFilePath !== capabilityModelFilePath ) ; codeGenExecutions . codeGenExecutionItems . push ( codeGenExecutionInfo ) ; } } else { codeGenExecutions = { codeGenExecutionItems : [ codeGenExecutionInfo ] } ; } if ( codeGenExecutions ) { await FileUtility . writeJsonFile ( type , codeGenConfigPath , codeGenExecutions ) ; } } catch ( error ) { console . log ( error ) ; } } private async getCodeGenCliPackageInfo ( context , channel ) { const extensionPackage = await Utility . getJsonContent ( context . asAbsolutePath ( "./package.json" ) ) ; const extensionVersion = extensionPackage . version ; const options = { method : "GET" , uri : extensionPackage . codeGenConfigUrl , encoding : "utf8" , json : true } ; let targetConfigItem = null ; const codeGenConfig = await request ( options ) . promise ( ) ; if ( codeGenConfig ) { codeGenConfig . codeGeneratorConfigItems . sort ( ( configItem1 , configItem2 ) => { return compareVersion ( configItem2 . codeGeneratorVersion , configItem1 . codeGeneratorVersion ) ; } ) ; if ( ! /^[0-9]+\.[0-9]+\.[0-9]+$/ . test ( extensionVersion ) ) { targetConfigItem = codeGenConfig . codeGeneratorConfigItems [ 0 ] ; } else { for ( const item of codeGenConfig . codeGeneratorConfigItems ) { if ( compareVersion ( extensionVersion , item . iotWorkbenchMinimalVersion ) >= 0 ) { targetConfigItem = item ; break ; } } } } if ( ! targetConfigItem ) { const message = `${ DigitalTwinConstants . dtPrefix } \
      Failed to get download information for ${ DigitalTwinConstants . codeGenCli }.` ; utils . channelShowAndAppendLine ( channel , message ) ; } return targetConfigItem ; } private async checkLocalCodeGenCli ( ) { const platform = os . platform ( ) ; const currentVersion = ConfigHandler . get < string > ( ConfigKey . codeGeneratorVersion ) ; let codeGenCliAppPath = path . join ( localCodeGenCliPath ( ) , DigitalTwinConstants . codeGenCliApp ) ; if ( platform === OSPlatform . WIN32 ) { codeGenCliAppPath += ".exe" ; } if ( ! fs . isFileSync ( codeGenCliAppPath ) || currentVersion == null ) { return null ; } return currentVersion ; } private async downloadAndInstallCodeGenCli ( channel , targetConfigItem , installOrUpgrade , newVersion ) { let packageUri ; let md5value ; const platform = os . platform ( ) ; if ( platform === OSPlatform . WIN32 ) { packageUri = targetConfigItem . codeGeneratorLocation . win32PackageUrl ; md5value = targetConfigItem . codeGeneratorLocation . win32Md5 ; } else if ( platform === OSPlatform . DARWIN ) { packageUri = targetConfigItem . codeGeneratorLocation . macOSPackageUrl ; md5value = targetConfigItem . codeGeneratorLocation . macOSMd5 ; } else { packageUri = targetConfigItem . codeGeneratorLocation . ubuntuPackageUrl ; md5value = targetConfigItem . codeGeneratorLocation . ubuntuMd5 ; } utils . channelShowAndAppend ( channel , `Step 1: Downloading ${ DigitalTwinConstants . codeGenCli } v${ newVersion } package ...` ) ; const downloadOption = { method : "GET" , uri : packageUri , encoding : null } ; const zipData = await request ( downloadOption ) . promise ( ) ; const tempPath = path . join ( os . tmpdir ( ) , FileNames . iotworkbenchTempFolder ) ; const filePath = path . join ( tempPath , `${ md5value }.zip` ) ; fs . writeFileSync ( filePath , zipData ) ; utils . channelShowAndAppendLine ( channel , " download complete." ) ; utils . channelShowAndAppend ( channel , "Step 2: Validating hash code for the package ..." ) ; const hashvalue = await FileUtility . getFileHash ( filePath ) ; if ( hashvalue !== md5value ) { utils . channelShowAndAppendLine ( channel , `the downloaded ${ DigitalTwinConstants . codeGenCli } v${ newVersion } package has been corrupted.` ) ; if ( installOrUpgrade === CodeGenCliOperation . Install ) { utils . channelShowAndAppendLine ( channel , `${ DigitalTwinConstants . dtPrefix } Abort generating device code stub.` ) ; return false ; } else { utils . channelShowAndAppendLine ( channel , `        Abort the installation and continue generating device code stub.` ) ; return true ; } } else { utils . channelShowAndAppendLine ( channel , " passed." ) ; } const codeGenCommandPath = localCodeGenCliPath ( ) ; utils . channelShowAndAppend ( channel , `Step 3: Extracting files ...` ) ; await FileUtility . extractZipFile ( filePath , codeGenCommandPath ) ; utils . channelShowAndAppendLine ( channel , " done." ) ; await ConfigHandler . update ( ConfigKey . codeGeneratorVersion , newVersion , vscode . ConfigurationTarget . Global ) ; utils . channelShowAndAppendLine ( channel , `${ DigitalTwinConstants . dtPrefix } The ${ DigitalTwinConstants . codeGenCli } v${ newVersion } is ready to use.` ) ; return true ; } private async installOrUpgradeCodeGenCli ( context , channel ) { utils . channelShowAndAppend ( channel , `${ DigitalTwinConstants . dtPrefix } Check ${ DigitalTwinConstants . codeGenCli } ...` ) ; const targetConfigItem = await this . getCodeGenCliPackageInfo ( context , channel ) ; if ( targetConfigItem === null ) { return false ; } let installOrUpgrade = CodeGenCliOperation . None ; const currentVersion = await this . checkLocalCodeGenCli ( ) ; if ( currentVersion == null ) { installOrUpgrade = CodeGenCliOperation . Install ; } else { if ( compareVersion ( targetConfigItem . codeGeneratorVersion , currentVersion ) > 0 ) { installOrUpgrade = CodeGenCliOperation . Upgrade ; } } if ( installOrUpgrade === CodeGenCliOperation . None ) { utils . channelShowAndAppendLine ( channel , `CodeGen CLI v${ currentVersion } is installed and ready to use.` ) ; return true ; } const newVersion = targetConfigItem . codeGeneratorVersion ; const processTitle = installOrUpgrade === CodeGenCliOperation . Install ? `Installing ${ DigitalTwinConstants . codeGenCli }` : `Upgrading ${ DigitalTwinConstants . codeGenCli }` ; const message = installOrUpgrade === CodeGenCliOperation . Install ? ` not installed, start installing:` : ` new version detected, start upgrading from ${ currentVersion } to ${ newVersion }:` ; utils . channelShowAndAppendLine ( channel , message ) ; let result = false ; await vscode . window . withProgress ( { location : vscode . ProgressLocation . Notification , title : processTitle } , async ( progress ) => { progress . report ( { increment : 0 } ) ; setTimeout ( ( ) => { progress . report ( { increment : 10 , message : `still going...` } ) ; } , 1000 ) ; setTimeout ( ( ) => { progress . report ( { increment : 40 , message : `still going...` } ) ; } , 5000 ) ; setTimeout ( ( ) => { progress . report ( { increment : 50 , message : `almost done...` } ) ; } , 10000 ) ; result = await this . downloadAndInstallCodeGenCli ( channel , targetConfigItem as CodeGeneratorConfigItem , installOrUpgrade , newVersion ) ; } ) ; return result ; } }