Skip to content

Commit

Permalink
fix: update gitignore, detect missing func runtime (microsoft#7488)
Browse files Browse the repository at this point in the history
* Fix microsoft#7473: add generated/ to .gitignore

* fix microsoft#7471: detect missing func runtime

* fix error link text

Co-authored-by: Soroush <hatpick@gmail.com>
  • Loading branch information
benbrown and hatpick authored Apr 29, 2021
1 parent 1ad17e4 commit e5010ef
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand Down
33 changes: 28 additions & 5 deletions Composer/packages/client/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ import { Toolbar, IToolbarItem } from '@bfc/ui-shared';

import { CreationFlowStatus } from '../../constants';
import { dispatcherState } from '../../recoilModel';
import { recentProjectsState, feedState, warnAboutDotNetState } from '../../recoilModel/atoms/appState';
import {
recentProjectsState,
feedState,
warnAboutDotNetState,
warnAboutFunctionsState,
} from '../../recoilModel/atoms/appState';
import TelemetryClient from '../../telemetry/TelemetryClient';
import composerDocumentIcon from '../../images/composerDocumentIcon.svg';
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';
Expand Down Expand Up @@ -73,10 +78,15 @@ const Home: React.FC<RouteComponentProps> = () => {

const recentProjects = useRecoilValue(recentProjectsState);
const feed = useRecoilValue(feedState);
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) {
Expand Down Expand Up @@ -257,6 +267,19 @@ const Home: React.FC<RouteComponentProps> = () => {
onDismiss={() => setWarnAboutDotNet(false)}
/>
)}
{warnAboutFunctions && (
<InstallDepModal
downloadLink={missingFunctionsError.link.url}
downloadLinkText={formatMessage('Install Azure Functions')}
learnMore={{
text: formatMessage('Learn more'),
link: missingFunctionsError.linkAfterMessage.url,
}}
text={missingFunctionsError.message}
title={formatMessage('Azure Functions required')}
onDismiss={() => setWarnAboutFunctions(false)}
/>
)}
</div>
);
};
Expand Down
5 changes: 5 additions & 0 deletions Composer/packages/client/src/recoilModel/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,8 @@ export const warnAboutDotNetState = atom<boolean>({
key: getFullyQualifiedKey('warnAboutDotNetState'),
default: false,
});

export const warnAboutFunctionsState = atom<boolean>({
key: getFullyQualifiedKey('warnAboutFunctionsState'),
default: false,
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
selectedTemplateReadMeState,
showCreateQnAFromUrlDialogState,
warnAboutDotNetState,
warnAboutFunctionsState,
settingsState,
} from '../atoms';
import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../selectors';
Expand Down Expand Up @@ -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,
Expand All @@ -696,6 +701,7 @@ export const projectDispatcher = () => {
setCurrentProjectId,
setProjectError,
setWarnAboutDotNet,
setWarnAboutFunctions,
fetchReadMe,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') });
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -77,6 +77,7 @@ import {
botEndpointsState,
dispatcherState,
warnAboutDotNetState,
warnAboutFunctionsState,
} from '../../atoms';
import * as botstates from '../../atoms/botState';
import lgWorker from '../../parsers/lgWorker';
Expand Down Expand Up @@ -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);
}
};
Expand Down
17 changes: 17 additions & 0 deletions Composer/packages/client/src/utils/runtimeErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 Azure Functions'),
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);
};
33 changes: 32 additions & 1 deletion Composer/packages/server/src/services/project.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -54,6 +58,15 @@ function fixOldBotProjectMapEntries(
return map;
}

const isFunctionsRuntimeInstalled = async (): Promise<boolean> => {
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[] = [];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
});
}
}
}
1 change: 1 addition & 0 deletions Composer/packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
5 changes: 4 additions & 1 deletion extensions/samples/assets/shared/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# prevent appsettings.json get checked in
**/appsettings.json
**/appsettings.json

# files generated during the lubuild process
generated/

0 comments on commit e5010ef

Please sign in to comment.