authelia/test/helpers/context/AutheliaServerWithHotReload.ts

149 lines
4.6 KiB
TypeScript

import Chokidar from 'chokidar';
import fs from 'fs';
import { exec } from '../utils/exec';
import ChildProcess from 'child_process';
import kill from 'tree-kill';
import AutheliaServerInterface from './AutheliaServerInterface';
class AutheliaServerWithHotReload implements AutheliaServerInterface {
private watcher: Chokidar.FSWatcher;
private configPath: string;
private AUTHELIA_INTERRUPT_FILENAME = '.authelia-interrupt';
private serverProcess: ChildProcess.ChildProcess | undefined;
private clientProcess: ChildProcess.ChildProcess | undefined;
constructor(configPath: string) {
this.configPath = configPath;
this.watcher = Chokidar.watch(['server', 'shared/**/*.ts', 'node_modules',
this.AUTHELIA_INTERRUPT_FILENAME, configPath], {
persistent: true,
ignoreInitial: true,
});
}
private async startServer() {
await exec('./node_modules/.bin/tslint -c server/tslint.json -p server/tsconfig.json')
this.serverProcess = ChildProcess.spawn('./node_modules/.bin/ts-node',
['-P', './server/tsconfig.json', './server/src/index.ts', this.configPath], {
env: {...process.env},
});
this.serverProcess.stdout.pipe(process.stdout);
this.serverProcess.stderr.pipe(process.stderr);
this.serverProcess.on('exit', () => {
console.log('Authelia server exited.');
if (!this.serverProcess) return;
this.serverProcess.removeAllListeners();
this.serverProcess = undefined;
});
}
private killServer() {
return new Promise((resolve, reject) => {
if (this.serverProcess) {
try {
const timeout = setTimeout(
() => reject(new Error('Server not killed after timeout.')), 10000);
this.serverProcess.on('exit', () => {
clearTimeout(timeout);
resolve();
});
kill(this.serverProcess.pid, 'SIGKILL');
} catch (e) {
reject(e);
}
} else {
resolve();
}
});
}
private async startClient() {
this.clientProcess = ChildProcess.spawn('npm', ['run', 'start'], {
cwd: './client',
env: {
...process.env,
'BROWSER': 'none'
}
});
this.clientProcess.stdout.pipe(process.stdout);
this.clientProcess.stderr.pipe(process.stderr);
this.clientProcess.on('exit', () => {
console.log('Authelia client exited.');
if (!this.clientProcess) return;
this.clientProcess.removeAllListeners();
this.clientProcess = undefined;
})
}
private killClient() {
return new Promise((resolve, reject) => {
if (this.clientProcess) {
try {
const timeout = setTimeout(
() => reject(new Error('Server not killed after timeout.')), 10000);
this.clientProcess.on('exit', () => {
clearTimeout(timeout);
resolve();
});
kill(this.clientProcess.pid, 'SIGKILL');
} catch (e) {
reject(e);
}
} else {
resolve();
}
});
}
private async generateConfigurationSchema() {
await exec('./node_modules/.bin/typescript-json-schema -o ' +
'server/src/lib/configuration/Configuration.schema.json ' +
'--strictNullChecks --required server/tsconfig.json Configuration');
}
/**
* Handle file changes.
* @param path The path of the file that has been changed.
*/
private async onFileChanged(path: string) {
console.log(`File ${path} has been changed, reloading...`);
if (path.startsWith('server/src/lib/configuration/schema')) {
console.log('Schema needs to be regenerated.');
await this.generateConfigurationSchema();
}
else if (path === this.AUTHELIA_INTERRUPT_FILENAME) {
if (fs.existsSync(path)) {
console.log('Authelia is being interrupted.');
await this.killServer();
} else {
console.log('Authelia is restarting.');
await this.startServer();
}
return;
}
await this.killServer();
await this.startServer();
}
async start() {
if (fs.existsSync(this.AUTHELIA_INTERRUPT_FILENAME)) {
console.log('Authelia is interrupted. Consider removing ' + this.AUTHELIA_INTERRUPT_FILENAME + ' if it\'s not expected.');
return;
}
console.log('Start watching file changes...');
this.watcher.on('add', (p) => this.onFileChanged(p));
this.watcher.on('unlink', (p) => this.onFileChanged(p));
this.watcher.on('change', (p) => this.onFileChanged(p));
this.startClient();
this.startServer();
}
async stop() {
await this.killClient();
await this.killServer();
}
}
export default AutheliaServerWithHotReload;