Skip to content

Commit

Permalink
#1194 - lazy invokation of ngrok for remote endopints
Browse files Browse the repository at this point in the history
  • Loading branch information
justinwilaby committed Jan 3, 2019
1 parent cd97ed9 commit efdf84d
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,6 @@ describe('The ServiceExplorerSagas', () => {
"type": "endpoint",
"name": "https://testbot.botframework.com/api/messagesv3",
"id": "https://testbot.botframework.com/api/messagesv3",
"appId": "51fc2648-1190-44fa-9559-87b11b1d0014",
"appPassword": "jxZjGcOpyfM4q75vp2paNQd",
"endpoint": "https://testbot.botframework.com/api/messagesv3"
}]
}`);
Expand Down
1 change: 0 additions & 1 deletion packages/app/main/src/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export class Emulator {

async startup() {
await this.framework.startup();
await this.ngrok.startup();
}

public report(conversationId: string) {
Expand Down
117 changes: 117 additions & 0 deletions packages/app/main/src/ngrokService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SettingsImpl } from '@bfemulator/app-shared';
import { combineReducers, createStore } from 'redux';
import { bot } from './botData/reducers/bot';
import * as store from './botData/store';
import { emulator } from './emulator';
import { NgrokService } from './ngrokService';
import { setFramework } from './settingsData/actions/frameworkActions';
import reducers from './settingsData/reducers';
import { getStore } from './settingsData/store';

jest.mock('./emulator', () => ({
emulator: {
framework: {
serverUrl: 'http://localhost:3000',
locale: 'en-us',
bypassNgrokLocalhost: true,
serverPort: 8080,
ngrokPath: '/usr/bin/ngrok',
server: {
botEmulator: {
facilities: {
conversations: {
getConversationIds: () => ['12', '123']
},
endpoints: {
reset: () => null,
push: () => null
}
}
}
}
}
}
}));

let mockSettingsStore;
const mockCreateStore = () => createStore(reducers);
const mockSettingsImpl = SettingsImpl;
jest.mock('./settingsData/store', () => ({
getStore: function () {
return mockSettingsStore || (mockSettingsStore = mockCreateStore());
},
getSettings: function () {
return new mockSettingsImpl(mockSettingsStore.getState());
},
get dispatch() {
return mockSettingsStore.dispatch;
}
}));

let mockStore;
(store as any).getStore = function () {
return mockStore || (mockStore = createStore(combineReducers({ bot })));
};

let mockCallsToLog = [];
jest.mock('./main', () => ({
mainWindow: {
logService: {
logToChat: (...args: any[]) => {
mockCallsToLog.push({ name: 'remoteCall', args });
}
}
}
}));

jest.mock('./ngrok', () => {
let connected = false;
return {
running: () => connected,
connect: (opts, cb) => (connected = true, cb(null, 'http://fdsfds.ngrok.io', 'http://fdsfds.ngrok.io')),
kill: (cb) => (connected = false, cb(null, !connected))
};
});

describe('The ngrokService', () => {
let ngrokService = new NgrokService();

beforeEach(() => {
getStore().dispatch(setFramework(emulator.framework as any));
mockCallsToLog.length = 0;
});

it('should be a singleton', () => {
expect(ngrokService).toBe(new NgrokService());
});

it('should not invoke ngrok for localhost urls', async () => {
const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3030/v3/messages');
expect(serviceUrl).toBe('http://localhost:8080');
});

it('should connect to ngrok when a remote endpoint is used', async () => {
const serviceUrl = await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
expect(serviceUrl).toBe('http://fdsfds.ngrok.io');
});

it ('should broadcast to each conversation that ngrok has reconnected', async() => {
await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
ngrokService.broadcastNgrokReconnected();
expect(mockCallsToLog.length).toBe(8);
});

it ('should report its status to the specified conversation when "report()" is called', async() => {
await ngrokService.getServiceUrl('http://myBot.someorg:3030/v3/messages');
ngrokService.report('12');
expect(mockCallsToLog.length).toBe(3);
});

it ('should reportNotConfigured() when no ngrokPath is specified', async () => {
(ngrokService as any)._ngrokPath = '';
ngrokService.report('12');
expect(mockCallsToLog.length).toBe(3);
expect(mockCallsToLog[0].args[1].payload.text).toBe(
'ngrok not configured (only needed when connecting to remotely hosted bots)');
});
});
61 changes: 28 additions & 33 deletions packages/app/main/src/ngrokService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { promisify } from 'util';

import { emulator } from './emulator';
import { getStore } from './settingsData/store';
import { isLocalhostUrl } from './utils';
import * as ngrok from './ngrok';
import { mainWindow } from './main';
import { FrameworkSettings } from '@bfemulator/app-shared';
import { ILogItem } from '@bfemulator/emulator-core/lib/types/log/item';
import LogLevel from '@bfemulator/emulator-core/lib/types/log/level';
import {
appSettingsItem,
Expand All @@ -46,30 +41,39 @@ import {
ngrokExpirationItem,
textItem
} from '@bfemulator/emulator-core/lib/types/log/util';
import { ILogItem } from '@bfemulator/emulator-core/lib/types/log/item';
import { FrameworkSettings } from '@bfemulator/app-shared';
import { promisify } from 'util';

import { emulator } from './emulator';
import { mainWindow } from './main';
import * as ngrok from './ngrok';
import { getStore } from './settingsData/store';
import { isLocalhostUrl } from './utils';

let ngrokInstance: NgrokService;

export class NgrokService {
private _ngrokPath: string;
private _serviceUrl: string;
private _inspectUrl: string;
private _spawnErr: any;
private _localhost: string;
private _bypass: boolean;
private _localhost = 'localhost';
private _triedToSpawn: boolean;

constructor() {
return ngrokInstance || (ngrokInstance = this); // Singleton
}

getServiceUrl(botUrl: string): string {
if (botUrl && isLocalhostUrl(botUrl) && this._bypass) {
public async getServiceUrl(botUrl: string): Promise<string> {
const bypassNgrokLocalhost = getStore().getState().framework.bypassNgrokLocalhost;
if (botUrl && isLocalhostUrl(botUrl) && bypassNgrokLocalhost) {
// Do not use ngrok
const port = emulator.framework.serverPort;

return `http://${this._localhost}:${port}`;
return `http://${ this._localhost }:${ port }`;
} else {
if (!ngrok.running()) {
await this.startup();
}
// Use ngrok if ngrok is up
return this._serviceUrl;
}
Expand All @@ -80,21 +84,10 @@ export class NgrokService {
err: this._spawnErr
})

public getNgrokServiceUrl(): string {
return this._serviceUrl;
}

public async startup() {
this.cacheSettings();
await this.recycle();
}

public async updateNgrokFromSettings(framework: FrameworkSettings) {
this.cacheSettings();
this._bypass = framework.bypassNgrokLocalhost;

if (this._ngrokPath !== framework.ngrokPath) {
await this.recycle();
if (this._ngrokPath !== framework.ngrokPath && ngrok.running()) {
return this.recycle();
}
}

Expand All @@ -108,7 +101,7 @@ export class NgrokService {
const port = emulator.framework.serverPort;

this._ngrokPath = getStore().getState().framework.ngrokPath;
this._serviceUrl = `http://${this._localhost}:${port}`;
this._serviceUrl = `http://${ this._localhost }:${ port }`;
this._inspectUrl = null;
this._spawnErr = null;
this._triedToSpawn = false;
Expand Down Expand Up @@ -137,7 +130,7 @@ export class NgrokService {
const bypassNgrokLocalhost = getStore().getState().framework.bypassNgrokLocalhost;
const { broadcast } = this;
broadcast(textItem(LogLevel.Debug, 'ngrok reconnected.'));
broadcast(textItem(LogLevel.Debug, `ngrok listening on ${this._serviceUrl}`));
broadcast(textItem(LogLevel.Debug, `ngrok listening on ${ this._serviceUrl }`));
broadcast(
textItem(LogLevel.Debug, 'ngrok traffic inspector:'),
externalLinkItem(this._inspectUrl, this._inspectUrl)
Expand Down Expand Up @@ -174,6 +167,11 @@ export class NgrokService {
}
}

private async startup() {
this.cacheSettings();
await this.recycle();
}

/** Logs messages that tell the user that ngrok isn't configured */
private reportNotConfigured(conversationId: string): void {
mainWindow.logService.logToChat(conversationId,
Expand All @@ -187,7 +185,7 @@ export class NgrokService {
private reportRunning(conversationId: string): void {
const bypassNgrokLocalhost = getStore().getState().framework.bypassNgrokLocalhost;
mainWindow.logService.logToChat(conversationId,
textItem(LogLevel.Debug, `ngrok listening on ${this._serviceUrl}`));
textItem(LogLevel.Debug, `ngrok listening on ${ this._serviceUrl }`));
mainWindow.logService.logToChat(conversationId,
textItem(LogLevel.Debug, 'ngrok traffic inspector:'),
externalLinkItem(this._inspectUrl, this._inspectUrl));
Expand Down Expand Up @@ -216,9 +214,6 @@ export class NgrokService {
// port = +parts[1].trim();
}
this._localhost = hostname;

// Cache bypass ngrok for local bots
this._bypass = framework.bypassNgrokLocalhost || true;
}
}

Expand Down
15 changes: 7 additions & 8 deletions packages/app/main/src/restServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@
//

import { BotEmulator, Conversation } from '@bfemulator/emulator-core';
import CORS from 'restify-cors-middleware';
import LogLevel from '@bfemulator/emulator-core/lib/types/log/level';
import { networkRequestItem, networkResponseItem, textItem } from '@bfemulator/emulator-core/lib/types/log/util';
import * as Restify from 'restify';
import CORS from 'restify-cors-middleware';
import { emulator } from './emulator';

import { mainWindow } from './main';
import { emulator } from './emulator';
import { networkRequestItem, networkResponseItem, textItem } from '@bfemulator/emulator-core/lib/types/log/util';
import LogLevel from '@bfemulator/emulator-core/lib/types/log/level';

export class RestServer {
private readonly _botEmulator: BotEmulator;
Expand Down Expand Up @@ -104,7 +104,7 @@ export class RestServer {
),
textItem(
level,
`${facility}.${routeName}`
`${ facility }.${ routeName }`
),
);
});
Expand All @@ -113,11 +113,10 @@ export class RestServer {
this._router.use(cors.actual);

this._botEmulator = new BotEmulator(
botUrl => emulator.ngrok.getServiceUrl(botUrl),
async (botUrl: string) => emulator.ngrok.getServiceUrl(botUrl),
{
fetch: fetch,
loggerOrLogService: mainWindow.logService,
tunnelingServiceUrl: () => emulator.ngrok.getNgrokServiceUrl()
loggerOrLogService: mainWindow.logService
}
);

Expand Down
3 changes: 1 addition & 2 deletions packages/emulator/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ async function main() {

// Create a bot entry
const bot = new BotEmulator(
program.serviceUrl || `http://localhost:${ port }`,
async () => program.serviceUrl || `http://localhost:${ port }`,
{
tunnelingServiceUrl: '',
loggerOrLogService: new NpmLogger()
}
);
Expand Down
37 changes: 13 additions & 24 deletions packages/emulator/core/src/botEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,32 @@
//

import * as Restify from 'restify';
import registerAttachmentRoutes from './attachments/registerRoutes';
import registerBotStateRoutes from './botState/registerRoutes';
import registerConversationRoutes from './conversations/registerRoutes';
import registerDirectLineRoutes from './directLine/registerRoutes';
import registerEmulatorRoutes from './emulator/registerRoutes';
import Attachments from './facility/attachments';
import BotState from './facility/botState';
import ConsoleLogService from './facility/consoleLogService';
import ConversationSet from './facility/conversationSet';
import EndpointSet from './facility/endpointSet';
import BotEmulatorOptions from './types/botEmulatorOptions';
import Logger from './types/logger';
import LogService from './types/log/service';
import LoggerAdapter from './facility/loggerAdapter';
import registerAttachmentRoutes from './attachments/registerRoutes';
import registerBotStateRoutes from './botState/registerRoutes';
import registerConversationRoutes from './conversations/registerRoutes';
import registerDirectLineRoutes from './directLine/registerRoutes';

import Users from './facility/users';
import registerSessionRoutes from './session/registerRoutes';
import BotEmulatorOptions from './types/botEmulatorOptions';
import LogService from './types/log/service';
import Logger from './types/logger';
import registerUserTokenRoutes from './userToken/registerRoutes';
import stripEmptyBearerToken from './utils/stripEmptyBearerToken';
import registerEmulatorRoutes from './emulator/registerRoutes';

import Users from './facility/users';

const DEFAULT_OPTIONS: BotEmulatorOptions = {
fetch,
loggerOrLogService: new ConsoleLogService(),
stateSizeLimitKB: 64
};

export interface ServiceUrlProvider {
(botUrl: string): string;
}

export interface Facilities {
attachments: Attachments;
botState: BotState;
Expand All @@ -75,19 +71,12 @@ export interface Facilities {
export default class BotEmulator {
// TODO: Instead of providing a getter for serviceUrl, we should let the upstream to set the serviceUrl
// Currently, the upstreamer doesn't really know when the serviceUrl change (ngrok), they need to do their job
public getServiceUrl: ServiceUrlProvider;
public getServiceUrl: (botUrl: string) => Promise<string>;
public options: BotEmulatorOptions;
public facilities: Facilities;

constructor(
public serviceUrlOrProvider: string | ServiceUrlProvider,
options: BotEmulatorOptions = DEFAULT_OPTIONS
) {
if (typeof serviceUrlOrProvider === 'string') {
this.getServiceUrl = () => serviceUrlOrProvider;
} else {
this.getServiceUrl = serviceUrlOrProvider;
}
constructor(getServiceUrl: (botUrl: string) => Promise<string>, options: BotEmulatorOptions = DEFAULT_OPTIONS) {
this.getServiceUrl = getServiceUrl;

this.options = { ...DEFAULT_OPTIONS, ...options };

Expand Down
Loading

0 comments on commit efdf84d

Please sign in to comment.