From 8dc487fe95fa2bbec418550dabc343c7924bd581 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Thu, 29 Apr 2021 11:36:23 -0500 Subject: [PATCH 1/3] Fix #7473: add generated/ to .gitignore --- extensions/samples/assets/shared/.gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/samples/assets/shared/.gitignore b/extensions/samples/assets/shared/.gitignore index b658b87ba9..9c49f140c9 100644 --- a/extensions/samples/assets/shared/.gitignore +++ b/extensions/samples/assets/shared/.gitignore @@ -1,2 +1,5 @@ # prevent appsettings.json get checked in -**/appsettings.json \ No newline at end of file +**/appsettings.json + +# files generated during the lubuild process +generated/ \ No newline at end of file From 0b6ae7b3cd0b0c9ddb3a8e8d78b3c1d39f3e17d8 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Thu, 29 Apr 2021 14:54:17 -0500 Subject: [PATCH 2/3] fix #7471: detect missing func runtime --- Composer/packages/client/src/constants.ts | 3 ++ .../packages/client/src/pages/home/Home.tsx | 27 ++++++++++++--- .../client/src/recoilModel/atoms/appState.ts | 5 +++ .../src/recoilModel/dispatchers/project.ts | 6 ++++ .../src/recoilModel/dispatchers/publisher.ts | 10 +++++- .../recoilModel/dispatchers/utils/project.ts | 9 ++++- .../client/src/utils/runtimeErrors.ts | 17 ++++++++++ .../packages/server/src/services/project.ts | 33 ++++++++++++++++++- Composer/packages/types/src/telemetry.ts | 1 + 9 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index 1fb8bdda9e..b5dd7df4b7 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -95,6 +95,9 @@ export const Text = { get DOTNETFAILURE() { return formatMessage('Composer needs .NET Core SDK'); }, + get FUNCTIONSFAILURE() { + return formatMessage('Composer needs Azure Functions'); + }, get BOTRUNTIMEERROR() { return formatMessage('Composer Runtime Error'); }, diff --git a/Composer/packages/client/src/pages/home/Home.tsx b/Composer/packages/client/src/pages/home/Home.tsx index 42d08c475c..fa6be2f2ba 100644 --- a/Composer/packages/client/src/pages/home/Home.tsx +++ b/Composer/packages/client/src/pages/home/Home.tsx @@ -22,6 +22,7 @@ import { templateIdState, currentProjectIdState, warnAboutDotNetState, + warnAboutFunctionsState, } from '../../recoilModel/atoms/appState'; import TelemetryClient from '../../telemetry/TelemetryClient'; import composerDocumentIcon from '../../images/composerDocumentIcon.svg'; @@ -29,7 +30,7 @@ import stackoverflowIcon from '../../images/stackoverflowIcon.svg'; import githubIcon from '../../images/githubIcon.svg'; import noRecentBotsCover from '../../images/noRecentBotsCover.svg'; import { InstallDepModal } from '../../components/InstallDepModal'; -import { missingDotnetVersionError } from '../../utils/runtimeErrors'; +import { missingDotnetVersionError, missingFunctionsError } from '../../utils/runtimeErrors'; import { RecentBotList } from './RecentBotList'; import { WhatsNewsList } from './WhatsNewsList'; @@ -73,10 +74,15 @@ const Home: React.FC = () => { const recentProjects = useRecoilValue(recentProjectsState); const feed = useRecoilValue(feedState); const templateId = useRecoilValue(templateIdState); - const { openProject, setCreationFlowStatus, setCreationFlowType, setWarnAboutDotNet } = useRecoilValue( - dispatcherState - ); + const { + openProject, + setCreationFlowStatus, + setCreationFlowType, + setWarnAboutDotNet, + setWarnAboutFunctions, + } = useRecoilValue(dispatcherState); const warnAboutDotNet = useRecoilValue(warnAboutDotNetState); + const warnAboutFunctions = useRecoilValue(warnAboutFunctionsState); const onItemChosen = async (item) => { if (item?.path) { @@ -250,6 +256,19 @@ const Home: React.FC = () => { onDismiss={() => setWarnAboutDotNet(false)} /> )} + {warnAboutFunctions && ( + setWarnAboutFunctions(false)} + /> + )} ); }; diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index ab34dd19b9..51b94af339 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -354,3 +354,8 @@ export const warnAboutDotNetState = atom({ key: getFullyQualifiedKey('warnAboutDotNetState'), default: false, }); + +export const warnAboutFunctionsState = atom({ + key: getFullyQualifiedKey('warnAboutFunctionsState'), + default: false, +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 41ff17f022..0c3b11782b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -37,6 +37,7 @@ import { selectedTemplateReadMeState, showCreateQnAFromUrlDialogState, warnAboutDotNetState, + warnAboutFunctionsState, settingsState, } from '../atoms'; import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../selectors'; @@ -672,6 +673,10 @@ export const projectDispatcher = () => { callbackHelpers.set(warnAboutDotNetState, warn); }); + const setWarnAboutFunctions = useRecoilCallback((callbackHelpers: CallbackInterface) => (warn: boolean) => { + callbackHelpers.set(warnAboutFunctionsState, warn); + }); + return { openProject, createNewBot, @@ -696,6 +701,7 @@ export const projectDispatcher = () => { setCurrentProjectId, setProjectError, setWarnAboutDotNet, + setWarnAboutFunctions, fetchReadMe, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index df9d590854..f77fe39563 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -28,6 +28,7 @@ import * as luUtil from '../../utils/luUtil'; import * as qnaUtil from '../../utils/qnaUtil'; import { ClientStorage } from '../../utils/storage'; import { RuntimeOutputData } from '../types'; +import { checkIfFunctionsMissing, missingFunctionsError } from '../../utils/runtimeErrors'; import { BotStatus, Text } from './../../constants'; import httpClient from './../../utils/httpUtil'; @@ -125,7 +126,14 @@ export const publisherDispatcher = () => { set(botStatusState(projectId), BotStatus.starting); } else if (status === PUBLISH_FAILED) { set(botStatusState(projectId), BotStatus.failed); - set(botBuildTimeErrorState(projectId), { ...data, title: formatMessage('Error occurred building the bot') }); + if (checkIfFunctionsMissing(data)) { + set(botBuildTimeErrorState(projectId), { + ...missingFunctionsError, + title: formatMessage('Error occurred building the bot'), + }); + } else { + set(botBuildTimeErrorState(projectId), { ...data, title: formatMessage('Error occurred building the bot') }); + } } } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 8e2b9c38a5..bd7f3d7e69 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -35,7 +35,7 @@ import { CallbackInterface } from 'recoil'; import { v4 as uuid } from 'uuid'; import isEmpty from 'lodash/isEmpty'; -import { checkIfDotnetVersionMissing } from '../../../utils/runtimeErrors'; +import { checkIfDotnetVersionMissing, checkIfFunctionsMissing } from '../../../utils/runtimeErrors'; import { BASEURL, BotStatus } from '../../../constants'; import settingStorage from '../../../utils/dialogSettingStorage'; import { getUniqueName } from '../../../utils/fileUtil'; @@ -77,6 +77,7 @@ import { botEndpointsState, dispatcherState, warnAboutDotNetState, + warnAboutFunctionsState, } from '../../atoms'; import * as botstates from '../../atoms/botState'; import lgWorker from '../../parsers/lgWorker'; @@ -394,11 +395,17 @@ export const handleProjectFailure = (callbackHelpers: CallbackInterface, error) const isDotnetError = checkIfDotnetVersionMissing({ message: error.response?.data?.message ?? error.message ?? '', }); + const isFunctionsError = checkIfFunctionsMissing({ + message: error.response?.data?.message ?? error.message ?? '', + }); if (isDotnetError) { callbackHelpers.set(warnAboutDotNetState, true); + } else if (isFunctionsError) { + callbackHelpers.set(warnAboutFunctionsState, true); } else { callbackHelpers.set(warnAboutDotNetState, false); + callbackHelpers.set(warnAboutFunctionsState, false); setError(callbackHelpers, error); } }; diff --git a/Composer/packages/client/src/utils/runtimeErrors.ts b/Composer/packages/client/src/utils/runtimeErrors.ts index dcf04e2fc4..7354abd709 100644 --- a/Composer/packages/client/src/utils/runtimeErrors.ts +++ b/Composer/packages/client/src/utils/runtimeErrors.ts @@ -15,6 +15,23 @@ export const missingDotnetVersionError = { }, }; +export const missingFunctionsError = { + message: formatMessage('To run this bot, Composer needs Azure Functions Core Tools.'), + linkAfterMessage: { + text: formatMessage('Learn more.'), + url: + 'https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools', + }, + link: { + text: formatMessage('Install Microsoft .NET Core SDK'), + url: 'https://www.npmjs.com/package/azure-functions-core-tools', + }, +}; + export const checkIfDotnetVersionMissing = (err: { message: string }) => { return /(Command failed: dotnet user-secrets)|(install[\w\r\s\S\t\n]*\.NET Core SDK)/.test(err.message as string); }; + +export const checkIfFunctionsMissing = (err: { message: string }) => { + return /(Azure Functions runtime not installed.)|(spawn func ENOENT)/.test(err.message as string); +}; diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts index b36a02c45c..6bd84b5d3a 100644 --- a/Composer/packages/server/src/services/project.ts +++ b/Composer/packages/server/src/services/project.ts @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { exec } from 'child_process'; +import { promisify } from 'util'; + import merge from 'lodash/merge'; import find from 'lodash/find'; import flatten from 'lodash/flatten'; @@ -23,6 +26,7 @@ import StorageService from './storage'; import { Path } from './../utility/path'; import { BackgroundProcessManager } from './backgroundProcessManager'; import { TelemetryService } from './telemetry'; +const execAsync = promisify(exec); const MAX_RECENT_BOTS = 7; @@ -54,6 +58,15 @@ function fixOldBotProjectMapEntries( return map; } +const isFunctionsRuntimeInstalled = async (): Promise => { + try { + const { stderr: funcErr } = await execAsync(`func -v`); + return !funcErr; + } catch (err) { + return false; + } +}; + export class BotProjectService { private static currentBotProjects: BotProject[] = []; private static recentBotProjects: LocationRef[] = []; @@ -475,6 +488,20 @@ export class BotProjectService { // TODO: Replace with default template once one is determined throw Error('empty templateID passed'); } + + // test for required dependencies + if (runtimeType === 'functions') { + if (!(await isFunctionsRuntimeInstalled())) { + BackgroundProcessManager.updateProcess(jobId, 500, formatMessage('Azure Functions runtime not installed.')); + TelemetryService.trackEvent('CreateNewBotProjectFailed', { + reason: 'Azure Functions runtime not installed.', + template: templateId, + status: 500, + }); + return; + } + } + // location to store the bot project const locationRef = getLocationRef(location, storageId, name); try { @@ -583,7 +610,11 @@ export class BotProjectService { const storage = StorageService.getStorageClient(locationRef.storageId, user); await storage.rmrfDir(locationRef.path); BackgroundProcessManager.updateProcess(jobId, 500, err instanceof Error ? err.message : err, err); - TelemetryService.trackEvent('CreateNewBotProjectCompleted', { template: templateId, status: 500 }); + TelemetryService.trackEvent('CreateNewBotProjectFailed', { + reason: err instanceof Error ? err.message : err, + template: templateId, + status: 500, + }); } } } diff --git a/Composer/packages/types/src/telemetry.ts b/Composer/packages/types/src/telemetry.ts index dfd80da5a7..87f8110869 100644 --- a/Composer/packages/types/src/telemetry.ts +++ b/Composer/packages/types/src/telemetry.ts @@ -71,6 +71,7 @@ type BotProjectEvents = { CreateNewBotProjectFromExample: { template: string }; CreateNewBotProjectStarted: { template: string }; CreateNewBotProjectCompleted: { template: string; status: number }; + CreateNewBotProjectFailed: { reason: string; template: string; status: number }; BotProjectOpened: { method: 'toolbar' | 'callToAction' | 'list'; projectId?: string }; StartAllBotsButtonClicked: undefined; StartBotButtonClicked: { isRoot: boolean; location: string; projectId: string }; From e919ef32df396d26451e8e1fd1d8bc77c76d1e88 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Thu, 29 Apr 2021 14:59:31 -0500 Subject: [PATCH 3/3] fix error link text --- Composer/packages/client/src/utils/runtimeErrors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/utils/runtimeErrors.ts b/Composer/packages/client/src/utils/runtimeErrors.ts index 7354abd709..32e34d7fa0 100644 --- a/Composer/packages/client/src/utils/runtimeErrors.ts +++ b/Composer/packages/client/src/utils/runtimeErrors.ts @@ -23,7 +23,7 @@ export const missingFunctionsError = { 'https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools', }, link: { - text: formatMessage('Install Microsoft .NET Core SDK'), + text: formatMessage('Install Azure Functions'), url: 'https://www.npmjs.com/package/azure-functions-core-tools', }, };