Simple web server component (#9648)

* Simple web server component

* More testing
This commit is contained in:
Amir Omidi
2020-03-17 16:32:06 -07:00
committed by GitHub
parent bd8c4a44c8
commit cc6cc2889a
4 changed files with 182 additions and 0 deletions

View File

@@ -198,6 +198,7 @@
"@azure/arm-resourcegraph": "^2.0.0",
"@azure/arm-subscriptions": "1.0.0",
"adal-node": "^0.2.1",
"axios": "^0.19.2",
"request": "2.88.0",
"vscode-nls": "^4.0.0"
},

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
import * as url from 'url';
import { AddressInfo } from 'net';
export type WebHandler = (req: http.IncomingMessage, reqUrl: url.UrlWithParsedQuery, res: http.ServerResponse) => void;
export class AlreadyRunningError extends Error { }
export class SimpleWebServer {
private hasStarted: boolean;
private readonly pathMappings = new Map<string, WebHandler>();
private readonly server: http.Server;
private lastUsed: number;
private shutoffInterval: NodeJS.Timer;
constructor(private readonly autoShutoffTimer = 5 * 60 * 1000) { // Default to five minutes.
this.bumpLastUsed();
this.autoShutoff();
this.server = http.createServer((req, res) => {
this.bumpLastUsed();
const reqUrl = url.parse(req.url!, /* parseQueryString */ true);
const handler = this.pathMappings.get(reqUrl.pathname);
if (handler) {
return handler(req, reqUrl, res);
}
res.writeHead(404);
res.end();
});
}
private bumpLastUsed(): void {
this.lastUsed = new Date().getTime();
}
public async shutdown(): Promise<void> {
clearInterval(this.shutoffInterval);
return new Promise<void>((resolve, reject) => {
this.server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
public async startup(): Promise<string> {
if (this.hasStarted) {
throw new AlreadyRunningError();
}
this.hasStarted = true;
let portTimeout: NodeJS.Timer;
const portPromise = new Promise<string>((resolve, reject) => {
portTimeout = setTimeout(() => {
reject(new Error('Timed out waiting for the server to start'));
}, 5000);
this.server.on('listening', () => {
// TODO: What are string addresses?
const address = this.server.address() as AddressInfo;
if (address!.port === undefined) {
reject(new Error('Port was not defined'));
}
resolve(address.port.toString());
});
this.server.on('error', () => {
reject(new Error('Server error'));
});
this.server.on('close', () => {
reject(new Error('Server closed'));
});
this.server.listen(0);
});
const clearPortTimeout = () => {
clearTimeout(portTimeout);
};
portPromise.finally(clearPortTimeout);
return portPromise;
}
public on(pathMapping: string, handler: WebHandler) {
this.pathMappings.set(pathMapping, handler);
}
private autoShutoff(): void {
this.shutoffInterval = setInterval(() => {
const time = new Date().getTime();
if (time - this.lastUsed > this.autoShutoffTimer) {
console.log('Shutting off webserver...');
this.shutdown().catch(console.error);
}
}, 1000);
}
}

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import 'mocha';
import axios from 'axios';
import { SimpleWebServer } from '../../../account-provider/utils/simpleWebServer';
let server: SimpleWebServer;
// These tests don't work on Linux systems because gnome-keyring doesn't like running on headless machines.
describe('AccountProvider.SimpleWebServer', function (): void {
beforeEach(async function (): Promise<void> {
server = new SimpleWebServer();
});
it('Starts up and returns a port', async function (): Promise<void> {
const port = await server.startup();
should(port).be.any.String().and.not.be.undefined().and.not.be.null();
});
it('Double startup should fail', async function (): Promise<void> {
await server.startup();
server.startup().should.be.rejected();
});
it('404 on unknown requests', async function (): Promise<void> {
const status = 404;
const server = new SimpleWebServer();
const port = await server.startup();
try {
const result = await axios.get(`http://localhost:${port}/hello`);
should(result).be.undefined();
} catch (ex) {
should(ex.response.status).equal(status);
}
});
it('Responds to GET requests', async function (): Promise<void> {
const msg = 'Hello World';
const status = 200;
const server = new SimpleWebServer();
const port = await server.startup();
server.on('/hello', (req, reqUrl, res) => {
res.writeHead(status);
res.write(msg);
res.end();
});
const response = await axios.get(`http://localhost:${port}/hello`);
should(response.status).equal(status);
should(response.data).equal(msg);
});
it('Server shuts off', async function (): Promise<void> {
await server.startup();
server.shutdown().should.not.be.rejected();
});
});

View File

@@ -237,6 +237,13 @@ axios@^0.19.0:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"