Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
2 changes: 1 addition & 1 deletion Composer/packages/types/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export type RuntimeTemplate = {
eject?: (project: IBotProject, localDisk?: any, isReplace?: boolean) => Promise<string>;

/** build method used for local publish */
build: (runtimePath: string, project: IBotProject) => Promise<void>;
build: (runtimePath: string, project: IBotProject, fullSettings?: DialogSetting, port?: string) => Promise<void>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

port being a string seems off. in which scenarios are fullSettings and port provided? Only functions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know port can be non-numeric in some cases. Would any be better?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string|number is an option. But, in what case would port be a string?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows app services listen on a named pipe which is expressed as a string. Unix sockets are also strings.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshgummersall are those used in these scenarios? We're not firing up unix sockets or windows app services when running the bot locally are we?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doubtful, but if this code interacts with deployments at all it may be wise to keep that option open. If it's purely for local execution of bots, good chance you can limit it to numbers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's all about the semantic of it: port is a 16-bit unsigned integer, thus ranging from 0 to 65535, representation of it in our app should follow this semantic imho. When and if we get to calling external services such as things mentioned above, we can always map to the expected type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will be present for calls to build on any of the new runtimes. However since we haven't yet deprecated the old runtime support, not every instance has been updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type of port updated to be a number


run: (project: IBotProject, localDisk?: any) => Promise<void>;

Expand Down
32 changes: 16 additions & 16 deletions extensions/localPublish/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ class LocalPublisher implements PublishPlugin<PublishConfig> {

private publishAsync = async (botId: string, version: string, fullSettings: any, project: any, user) => {
Comment thread
benbrown marked this conversation as resolved.
Outdated
try {
let port;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to move the port selection up in the code a bit so that it can be passed to the build step.

if (LocalPublisher.runningBots[botId]) {
this.composer.log('Bot already running. Stopping bot...');
// this may or may not be set based on the status of the bot
port = LocalPublisher.runningBots[botId].port;
await this.stopBot(botId);
}
if (!port) {
// Portfinder is the stablest amongst npm libraries for finding ports. https://github.com/http-party/node-portfinder/issues/61. It does not support supplying an array of ports to pick from as we can have a race conidtion when starting multiple bots at the same time. As a result, getting the max port number out of the range and starting the range from the max.
const maxPort = max(map(LocalPublisher.runningBots, 'port')) ?? 3979;
port = await portfinder.getPortPromise({ port: maxPort + 1, stopPort: 6000 });
}

// if enableCustomRuntime is not true, initialize the runtime code in a tmp folder
// and export the content into that folder as well.
const runtime = this.composer.getRuntimeByProject(project);
Expand All @@ -140,11 +153,11 @@ class LocalPublisher implements PublishPlugin<PublishConfig> {
await this.saveContent(botId, version, project.dataDir, user);
} else if (project.settings.runtime.path && project.settings.runtime.command) {
const runtimePath = project.getRuntimePath();
await runtime.build(runtimePath, project);
await runtime.build(runtimePath, project, fullSettings, port);
} else {
throw new Error('Custom runtime settings are incomplete. Please specify path and command.');
}
await this.setBot(botId, version, fullSettings, project);
await this.setBot(botId, version, fullSettings, project, port);
} catch (error) {
await this.stopBot(botId);
this.setBotStatus(botId, {
Expand Down Expand Up @@ -332,22 +345,9 @@ class LocalPublisher implements PublishPlugin<PublishConfig> {
};

// start bot in current version
private setBot = async (botId: string, version: string, settings: any, project: any) => {
private setBot = async (botId: string, version: string, settings: any, project: any, port: string) => {
// get port, and stop previous bot if exist
try {
let port;
if (LocalPublisher.runningBots[botId]) {
this.composer.log('Bot already running. Stopping bot...');
// this may or may not be set based on the status of the bot
port = LocalPublisher.runningBots[botId].port;
await this.stopBot(botId);
}
if (!port) {
// Portfinder is the stablest amongst npm libraries for finding ports. https://github.com/http-party/node-portfinder/issues/61. It does not support supplying an array of ports to pick from as we can have a race conidtion when starting multiple bots at the same time. As a result, getting the max port number out of the range and starting the range from the max.
const maxPort = max(map(LocalPublisher.runningBots, 'port')) ?? 3979;
port = await portfinder.getPortPromise({ port: maxPort + 1, stopPort: 6000 });
}

// if not using custom runtime, update assets in tmp older
if (!settings.runtime || settings.runtime.customRuntime !== true) {
this.composer.log('Updating bot assets');
Expand Down
62 changes: 58 additions & 4 deletions extensions/runtimes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,50 @@ import { IFileStorage } from './interface';
const execAsync = promisify(exec);
const removeDirAndFiles = promisify(rimraf);

/**
* Used to set values for Azure Functions runtime environment variables
* This is used to set the "sensitive values" when using Azure Functions
* @param name name of key
* @param value value of key
* @param cwd path where the command will be run
*/
const writeLocalFunctionsSetting = async (name: string, value: string, cwd: string) => {
// only set if there is both a setting and a value.
if (name && value && cwd) {
Comment thread
benbrown marked this conversation as resolved.
const { stderr: err } = await execAsync(`func settings add ${name} ${value}`, { cwd: cwd });
if (err) {
throw new Error(err);
}
}
};

const writeAllLocalFunctionsSettings = async (fullSettings, port, runtimePath) => {
await writeLocalFunctionsSetting('MicrosoftAppPassword', fullSettings.MicrosoftAppPassword, runtimePath);
await writeLocalFunctionsSetting(
'luis:endpointKey',
fullSettings.luis?.endpointKey || fullSettings.luis?.authoringKey,
runtimePath
);
await writeLocalFunctionsSetting('qna:endpointKey', fullSettings.qna?.endpointKey, runtimePath);
let skillHostEndpoint;
if (isSkillHostUpdateRequired(fullSettings?.skillHostEndpoint)) {
// Update skillhost endpoint only if ngrok url not set meaning empty or localhost url
skillHostEndpoint = `http://127.0.0.1:${port}/api/skills`;
}
await writeLocalFunctionsSetting('SkillHostEndpoint', skillHostEndpoint, runtimePath);
};

// eslint-disable-next-line security/detect-unsafe-regex
const localhostRegex = /^https?:\/\/(localhost|127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1)/;

const isLocalhostUrl = (matchUrl: string) => {
return localhostRegex.test(matchUrl);
};

const isSkillHostUpdateRequired = (skillHostEndpoint?: string) => {
return !skillHostEndpoint || isLocalhostUrl(skillHostEndpoint);
};

export default async (composer: any): Promise<void> => {
const dotnetTemplatePath = path.resolve(__dirname, '../../../runtime/dotnet');
const nodeTemplatePath = path.resolve(__dirname, '../../../runtime/node');
Expand Down Expand Up @@ -326,7 +370,7 @@ export default async (composer: any): Promise<void> => {
key: 'adaptive-runtime-dotnet-webapp',
name: 'C# - Web App',
build: async (runtimePath: string, _project: any) => {
composer.log(`BUILD THIS C# PROJECT! at ${runtimePath}...`);
composer.log(`BUILD THIS C# WEBAPP PROJECT! at ${runtimePath}...`);
composer.log('Run dotnet user-secrets init...');

// TODO: capture output of this and store it somewhere useful
Expand Down Expand Up @@ -456,10 +500,15 @@ export default async (composer: any): Promise<void> => {
name: 'C# - Functions',
// startCommand: 'dotnet run',
// path: dotnetTemplatePath,
build: async (runtimePath: string, _project: any) => {
composer.log(`BUILD THIS C# PROJECT! at ${runtimePath}...`);
build: async (runtimePath: string, _project: any, fullSettings?: any, port?: string) => {
Comment thread
benbrown marked this conversation as resolved.
Outdated
composer.log(`BUILD THIS C# FUNCTIONS PROJECT! at ${runtimePath}...`);
composer.log('Run dotnet user-secrets init...');

if (fullSettings && port) {
// we need to update the local.settings.json file with sensitive settings
await writeAllLocalFunctionsSettings(fullSettings, port, runtimePath);
}

// TODO: capture output of this and store it somewhere useful
const { stderr: initErr } = await execAsync(`dotnet user-secrets init --project ${_project.name}.csproj`, {
cwd: runtimePath,
Expand Down Expand Up @@ -629,7 +678,7 @@ export default async (composer: any): Promise<void> => {
composer.addRuntimeTemplate({
key: 'adaptive-runtime-js-functions',
name: 'JS - Functions (preview)',
build: async (runtimePath: string, _project: any) => {
build: async (runtimePath: string, _project: any, fullSettings?: any, port?: string) => {
// do stuff
composer.log('BUILD THIS JS PROJECT');
// install dev dependencies in production, make sure typescript is installed
Expand All @@ -642,6 +691,11 @@ export default async (composer: any): Promise<void> => {
composer.log(`npm install timeout, ${installErr}`);
}

if (fullSettings && port) {
// we need to update the local.settings.json file with sensitive settings
await writeAllLocalFunctionsSettings(fullSettings, port, runtimePath);
}

composer.log('BUILD COMPLETE');
},
installComponent: async (
Expand Down