From bf9bdbfee24c8b2e005d01a87bffb337cd34dfc0 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Mon, 15 Jan 2018 03:36:25 +0200 Subject: [PATCH] feat: stopping the server ref pnpm/pnpm#960 --- package.json | 1 + shrinkwrap.yaml | 8 +++++ src/connectStoreController.ts | 7 +++- src/createServer.ts | 26 ++++++++++++--- test/index.ts | 63 ++++++++++++++++++++++++++++++++--- 5 files changed, 95 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3b566199c6..f01364ddb0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@pnpm/npm-resolver": "^0.3.9", "@pnpm/tarball-fetcher": "^0.3.1", "@types/tape": "^4.2.31", + "is-port-reachable": "^2.0.0", "mos": "^2.0.0-alpha.3", "mos-plugin-readme": "^1.0.4", "package-preview": "^1.0.1", diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml index 3736809a0a..2d02926db3 100644 --- a/shrinkwrap.yaml +++ b/shrinkwrap.yaml @@ -13,6 +13,7 @@ devDependencies: '@pnpm/npm-resolver': 0.3.11 '@pnpm/tarball-fetcher': 0.3.3 '@types/tape': 4.2.31 + is-port-reachable: 2.0.0 mos: 2.0.0-alpha.3 mos-plugin-readme: 1.0.4 package-preview: 1.0.4 @@ -1711,6 +1712,12 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + /is-port-reachable/2.0.0: + dev: true + engines: + node: '>=4' + resolution: + integrity: sha1-VNE9ZUkX60M64+4ty8N3TyzUTrI= /is-promise/2.1.0: dev: true resolution: @@ -4000,6 +4007,7 @@ specifiers: '@types/tape': ^4.2.31 '@types/uuid': ^3.4.3 got: ^8.0.1 + is-port-reachable: ^2.0.0 mos: ^2.0.0-alpha.3 mos-plugin-readme: ^1.0.4 p-limit: ^1.1.0 diff --git a/src/connectStoreController.ts b/src/connectStoreController.ts index fc414fd16f..832f18d29c 100644 --- a/src/connectStoreController.ts +++ b/src/connectStoreController.ts @@ -10,12 +10,16 @@ import pLimit = require('p-limit') import {StoreController} from 'package-store' import uuid = require('uuid') +export type StoreServerController = StoreController & { + stop (): Promise, +} + export default function ( initOpts: { remotePrefix: string, concurrency?: number, }, -): Promise { +): Promise { const remotePrefix = initOpts.remotePrefix const limitedFetch = fetch.bind(null, pLimit(initOpts.concurrency || 100)) @@ -39,6 +43,7 @@ export default function ( saveState: async () => { await limitedFetch(`${remotePrefix}/saveState`, {}) }, + stop: () => limitedFetch(`${remotePrefix}/stop`, {}), updateConnections: async (prefix: string, opts: {addDependencies: string[], removeDependencies: string[], prune: boolean}) => { await limitedFetch(`${remotePrefix}/updateConnections`, { opts, diff --git a/src/createServer.ts b/src/createServer.ts index da6a8f1b62..506c8985e2 100644 --- a/src/createServer.ts +++ b/src/createServer.ts @@ -1,7 +1,10 @@ +import logger from '@pnpm/logger' +import { + RequestPackageOptions, + WantedDependency, +} from '@pnpm/package-requester' import http = require('http') import {IncomingMessage, Server, ServerResponse} from 'http' - -import {RequestPackageOptions, WantedDependency} from '@pnpm/package-requester' import {StoreController} from 'package-store' interface RequestBody { @@ -22,6 +25,7 @@ export default function ( path?: string, port?: number, hostname?: string, + ignoreStopRequests?: boolean, }, ) { const manifestPromises = {} @@ -97,6 +101,17 @@ export default function ( await store.importPackage(importPackageBody.from, importPackageBody.to, importPackageBody.opts) res.end(JSON.stringify('OK')) break + case '/stop': + if (opts.ignoreStopRequests) { + res.statusCode = 403 + res.end() + break + } + logger.info('Got request to stop the server') + await close() + res.end(JSON.stringify('OK')) + logger.info('Server stopped') + break default: res.statusCode = 404 res.end(`${req.url} does not match any route`) @@ -114,7 +129,10 @@ export default function ( listener = server.listen(opts.port, opts.hostname) } - return { - close: () => listener.close(() => { return }), + return { close } + + function close () { + listener.close() + return store.close() } } diff --git a/test/index.ts b/test/index.ts index cb806ef1e4..f13a419cac 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,12 +6,15 @@ import { import { PackageFilesResponse, } from '@pnpm/package-requester' +import got = require('got') +import isPortReachable = require('is-port-reachable') import createResolver from '@pnpm/npm-resolver' import createFetcher from '@pnpm/tarball-fetcher' import createStore from 'package-store' -test('server', async t => { - const registry = 'https://registry.npmjs.org/' +const registry = 'https://registry.npmjs.org/' + +async function createStoreController () { const rawNpmConfig = { registry } const store = '.store' const resolve = createResolver({ @@ -25,16 +28,19 @@ test('server', async t => { strictSsl: true, rawNpmConfig, }) - const storeCtrlForServer = await createStore(resolve, fetchers, { + return await createStore(resolve, fetchers, { networkConcurrency: 1, store: store, locks: undefined, lockStaleDuration: 100, }) +} +test('server', async t => { const port = 5813 const hostname = '127.0.0.1' const remotePrefix = `http://${hostname}:${port}` + const storeCtrlForServer = await createStoreController() const server = createServer(storeCtrlForServer, { port, hostname, @@ -45,7 +51,6 @@ test('server', async t => { { downloadPriority: 0, loggedPkg: {rawSpec: 'sfdf'}, - offline: false, prefix: process.cwd(), registry, verifyStoreIntegrity: false, @@ -65,7 +70,55 @@ test('server', async t => { await response['finishing'] - server.close() + await server.close() await storeCtrl.close() t.end() }) + +test('stop server with remote call', async t => { + const port = 5813 + const hostname = '127.0.0.1' + const remotePrefix = `http://${hostname}:${port}` + const storeCtrlForServer = await createStoreController() + const server = createServer(storeCtrlForServer, { + port, + hostname, + ignoreStopRequests: false, + }) + + t.ok(await isPortReachable(port), 'server is running') + + const response = await got(`${remotePrefix}/stop`, {method: 'POST'}) + + t.equal(response.statusCode, 200, 'success returned by server stopping endpoint') + + t.notOk(await isPortReachable(port), 'server is not running') + + t.end() +}) + +test('disallow stop server with remote call', async t => { + const port = 5813 + const hostname = '127.0.0.1' + const remotePrefix = `http://${hostname}:${port}` + const storeCtrlForServer = await createStoreController() + const server = createServer(storeCtrlForServer, { + port, + hostname, + ignoreStopRequests: true, + }) + + t.ok(await isPortReachable(port), 'server is running') + + try { + const response = await got(`${remotePrefix}/stop`, {method: 'POST'}) + t.fail('request should have failed') + } catch (err) { + t.equal(err.statusCode, 403, 'server not stopped') + } + + t.ok(await isPortReachable(port), 'server is running') + + await server.close() + t.end() +})