diff --git a/app/server/declarations.d.ts b/app/server/declarations.d.ts index 7d24946e203..3aa200204b8 100644 --- a/app/server/declarations.d.ts +++ b/app/server/declarations.d.ts @@ -6,7 +6,7 @@ declare module "app/server/lib/shutdown" { export function addCleanupHandler(context: T, method: (this: T) => void, timeout?: number, name?: string): void; export function removeCleanupHandlers(context: T): void; export function cleanupOnSignals(...signalNames: string[]): void; - export function exit(optExitCode?: number): Promise; + export function exit(optExitCode?: number, newEnv?: Record): Promise; } // There is a @types/bluebird, but it's not great, and breaks for some of our usages. diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 46e4a50847e..43e0c5e0ed6 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -75,6 +75,7 @@ import {buildWidgetRepository, getWidgetsInPlugins, IWidgetRepository} from 'app import {setupLocale} from 'app/server/localization'; import axios from 'axios'; import * as cookie from 'cookie'; +import EventEmitter from 'events'; import express from 'express'; import * as fse from 'fs-extra'; import * as http from 'http'; @@ -109,7 +110,7 @@ export interface FlexServerOptions { const noop: express.RequestHandler = (req, res, next) => next(); -export class FlexServer implements GristServer { +export class FlexServer extends EventEmitter implements GristServer { public readonly create = create; public tagChecker: TagChecker; public app: express.Express; @@ -186,6 +187,7 @@ export class FlexServer implements GristServer { constructor(public port: number, public name: string = 'flexServer', public readonly options: FlexServerOptions = {}) { + super() this.app = express(); this.app.set('port', port); @@ -624,6 +626,15 @@ export class FlexServer implements GristServer { } counter++; }); + + // Cargo-culted from above + this.on('restartWithNewEnv', (newEnv) => { + log.info('Restarting with new environment variables'); + if (counter === 0) { + void(shutdown.exit(0, newEnv)); + } + counter++; + }); } public addTagChecker() { @@ -1883,6 +1894,10 @@ export class FlexServer implements GristServer { const probes = new BootProbes(this.app, this, '/api', adminMiddleware); probes.addEndpoints(); + this.app.post('/api/admin/restart', requireInstallAdmin, expressWrap(async (req, resp) => { + return this.restartServer(req, resp); + })); + // Restrict this endpoint to install admins this.app.get('/api/install/prefs', requireInstallAdmin, expressWrap(async (_req, resp) => { const activation = await this._activations.current(); @@ -1939,6 +1954,14 @@ export class FlexServer implements GristServer { })); } + public async restartServer(req: express.Request, resp: express.Response) { + const newEnv = req.body.newEnv; + resp.on('finish', () => { + this.emit('restartWithNewEnv', newEnv); + }); + return resp.status(200).send(); + } + // Get the HTML template sent for document pages. public async getDocTemplate(): Promise { const page = await fse.readFile(path.join(getAppPathTo(this.appRoot, 'static'), diff --git a/app/server/lib/shutdown.js b/app/server/lib/shutdown.js index 104a227e579..08d14eb8da6 100644 --- a/app/server/lib/shutdown.js +++ b/app/server/lib/shutdown.js @@ -2,15 +2,15 @@ * Module for managing graceful shutdown. */ +const log = require('app/server/lib/log'); +const Promise = require('bluebird'); +const child_process = require("child_process"); -var log = require('app/server/lib/log'); -var Promise = require('bluebird'); +const os = require('os'); -var os = require('os'); +let cleanupHandlers = []; -var cleanupHandlers = []; - -var signalsHandled = {}; +let signalsHandled = {}; /** * Adds a handler that should be run on shutdown. @@ -97,7 +97,7 @@ function signalExit(signal) { * signals to the process. This should only be used for signals that normally kill the process. * E.g. cleanupOnSignals('SIGINT', 'SIGTERM', 'SIGUSR2'); */ -function cleanupOnSignals(varSignalNames) { +function cleanupOnSignals() { for (var i = 0; i < arguments.length; i++) { var signal = arguments[i]; if (signalsHandled[signal]) { continue; } @@ -110,13 +110,24 @@ exports.cleanupOnSignals = cleanupOnSignals; /** * Run cleanup handlers and exit the process with the given exit code (0 if omitted). */ -function exit(optExitCode) { +function exit(optExitCode, newEnv) { var prog = 'grist[' + process.pid + ']'; var code = optExitCode || 0; log.info("Server %s cleaning up", prog); return runCleanupHandlers() .finally(function() { log.info("Server %s exiting with code %s", prog, code); + if (newEnv !== undefined) { + process.on('exit', () => { + log.info("Server restarting with new environment"); + child_process.spawn(process.argv.shift(), process.argv, { + cwd: process.cwd(), + detached : true, + stdio: "inherit", + env: {...process.env, ...newEnv} + }); + }); + } process.exit(code); }); }