import {connectTestingHooks, TestingHooksClient} from "app/server/lib/TestingHooks"; import {ChildProcess, execFileSync, spawn} from "child_process"; import path from "path"; import * as fse from "fs-extra"; import * as testUtils from "test/server/testUtils"; import {exitPromise} from "app/server/lib/serverUtils"; import log from "app/server/lib/log"; import {delay} from "bluebird"; import fetch from "node-fetch"; export class TestServer { public static async startServer (serverTypes: string, tempDirectory: string, suitename: string, additionalConfig?: Object, _homeUrl?: string): Promise { const server = new TestServer(serverTypes, tempDirectory, suitename); // Override some env variables in server configuration to serve our test purpose: const customEnv = { ...additionalConfig}; await server.start(_homeUrl, customEnv); return server; } public testingSocket: string; public testingHooks: TestingHooksClient; public serverUrl: string; public stopped = false; private _server: ChildProcess; private _exitPromise: Promise; private readonly _defaultEnv; constructor(private _serverTypes: string, private _tmpDir: string, private _suiteName: string) { this._defaultEnv = { GRIST_INST_DIR: this._tmpDir, GRIST_SERVERS: this._serverTypes, // with port '0' no need to hard code a port number (we can use testing hooks to find out what // port server is listening on). GRIST_PORT: '0', GRIST_DISABLE_S3: 'true', REDIS_URL: process.env.TEST_REDIS_URL, GRIST_ALLOWED_HOSTS: `example.com,localhost`, GRIST_TRIGGER_WAIT_DELAY: '100', // this is calculated value, some tests expect 4 attempts and some will try 3 times GRIST_TRIGGER_MAX_ATTEMPTS: '4', GRIST_MAX_QUEUE_SIZE: '10', ...process.env }; } public async start(_homeUrl?: string, customEnv?: object) { // put node logs into files with meaningful name that relate to the suite name and server type const fixedName = this._serverTypes.replace(/,/, '_'); const nodeLogPath = path.join(this._tmpDir, `${this._suiteName}-${fixedName}-node.log`); const nodeLogFd = await fse.open(nodeLogPath, 'a'); const serverLog = process.env.VERBOSE ? 'inherit' : nodeLogFd; // use a path for socket that relates to suite name and server types this.testingSocket = path.join(this._tmpDir, `${this._suiteName}-${fixedName}.socket`); const env = { APP_HOME_URL: _homeUrl, GRIST_TESTING_SOCKET: this.testingSocket, ...this._defaultEnv, ...customEnv }; const main = await testUtils.getBuildFile('app/server/mergedServerMain.js'); this._server = spawn('node', [main, '--testingHooks'], { env, stdio: ['inherit', serverLog, serverLog] }); this._exitPromise = exitPromise(this._server); // Try to be more helpful when server exits by printing out the tail of its log. this._exitPromise.then((code) => { if (this._server.killed) { return; } log.error("Server died unexpectedly, with code", code); const output = execFileSync('tail', ['-30', nodeLogPath]); log.info(`\n===== BEGIN SERVER OUTPUT ====\n${output}\n===== END SERVER OUTPUT =====`); }) .catch(() => undefined); await this._waitServerReady(); log.info(`server ${this._serverTypes} up and listening on ${this.serverUrl}`); } public async stop() { if (this.stopped) { return; } log.info("Stopping node server: " + this._serverTypes); this.stopped = true; this._server.kill(); this.testingHooks.close(); await this._exitPromise; } public async isServerReady(): Promise { // Let's wait for the testingSocket to be created, then get the port the server is listening on, // and then do an api check. This approach allow us to start server with GRIST_PORT set to '0', // which will listen on first available port, removing the need to hard code a port number. try { // wait for testing socket while (!(await fse.pathExists(this.testingSocket))) { await delay(200); } // create testing hooks and get own port this.testingHooks = await connectTestingHooks(this.testingSocket); const port: number = await this.testingHooks.getOwnPort(); this.serverUrl = `http://localhost:${port}`; // wait for check return (await fetch(`${this.serverUrl}/status/hooks`, {timeout: 1000})).ok; } catch (err) { return false; } } private async _waitServerReady() { // It's important to clear the timeout, because it can prevent node from exiting otherwise, // which is annoying when running only this test for debugging. let timeout: any; const maxDelay = new Promise((resolve) => { timeout = setTimeout(resolve, 30000); }); try { await Promise.race([ this.isServerReady(), this._exitPromise.then(() => { throw new Error("Server exited while waiting for it"); }), maxDelay, ]); } finally { clearTimeout(timeout); } } }