mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Simple web server component (#9648)
* Simple web server component * More testing
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user