191 lines
6.0 KiB
TypeScript
191 lines
6.0 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';
|
|
import sleep from '../utils/sleep';
|
|
|
|
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;
|
|
private filesChangedBuffer: string[] = [];
|
|
private changeInProgress: boolean = false;
|
|
|
|
constructor(configPath: string, watchedPaths: string[]) {
|
|
this.configPath = configPath;
|
|
const pathsToReload = ['server', 'node_modules',
|
|
this.AUTHELIA_INTERRUPT_FILENAME, configPath].concat(watchedPaths);
|
|
console.log("Authelia will reload on changes of files or directories in " + pathsToReload.join(', '));
|
|
this.watcher = Chokidar.watch(pathsToReload, {
|
|
persistent: true,
|
|
ignoreInitial: true,
|
|
});
|
|
}
|
|
|
|
private async startServer() {
|
|
if (this.serverProcess) return;
|
|
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,
|
|
NODE_TLS_REJECT_UNAUTHORIZED: 0,
|
|
},
|
|
});
|
|
this.serverProcess.stdout.pipe(process.stdout);
|
|
this.serverProcess.stderr.pipe(process.stderr);
|
|
this.serverProcess.on('exit', () => {
|
|
if (!this.serverProcess) return;
|
|
console.log('Authelia server with pid=%s exited.', this.serverProcess.pid);
|
|
this.serverProcess.removeAllListeners();
|
|
this.serverProcess = undefined;
|
|
});
|
|
}
|
|
|
|
private killServer() {
|
|
return new Promise((resolve, reject) => {
|
|
if (this.serverProcess) {
|
|
const pid = this.serverProcess.pid;
|
|
try {
|
|
const timeout = setTimeout(
|
|
() => reject(new Error(`Server with pid=${pid} 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() {
|
|
if (this.clientProcess) return;
|
|
|
|
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', () => {
|
|
if (!this.clientProcess) return;
|
|
console.log('Authelia client exited with pid=%s.', this.clientProcess.pid);
|
|
this.clientProcess.removeAllListeners();
|
|
this.clientProcess = undefined;
|
|
})
|
|
}
|
|
|
|
private killClient() {
|
|
return new Promise((resolve, reject) => {
|
|
if (this.clientProcess) {
|
|
const pid = this.clientProcess.pid;
|
|
try {
|
|
const timeout = setTimeout(
|
|
() => reject(new Error(`Server with pid=${pid} 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 onFilesChanged = async (paths: string[]) => {
|
|
const containsSchemaFiles = paths.filter(
|
|
(p) => p.startsWith('server/src/lib/configuration/schema')).length > 0;
|
|
if (containsSchemaFiles) {
|
|
console.log('Schema needs to be regenerated.');
|
|
await this.generateConfigurationSchema();
|
|
}
|
|
|
|
const interruptFile = paths.filter(
|
|
(p) => p === this.AUTHELIA_INTERRUPT_FILENAME).length > 0;
|
|
if (interruptFile) {
|
|
if (fs.existsSync(this.AUTHELIA_INTERRUPT_FILENAME)) {
|
|
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();
|
|
|
|
if (this.filesChangedBuffer.length > 0) {
|
|
await this.consumeFileChanged();
|
|
}
|
|
}
|
|
|
|
private async consumeFileChanged() {
|
|
this.changeInProgress = true;
|
|
const paths = this.filesChangedBuffer;
|
|
this.filesChangedBuffer = [];
|
|
try {
|
|
await this.onFilesChanged(paths);
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
this.changeInProgress = false;
|
|
}
|
|
|
|
private enqueueFileChanged(path: string) {
|
|
console.log(`File ${path} has been changed, reloading...`);
|
|
this.filesChangedBuffer.push(path);
|
|
if (this.changeInProgress) return;
|
|
this.consumeFileChanged();
|
|
}
|
|
|
|
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.enqueueFileChanged(p));
|
|
this.watcher.on('unlink', (p) => this.enqueueFileChanged(p));
|
|
this.watcher.on('change', (p) => this.enqueueFileChanged(p));
|
|
|
|
this.startClient();
|
|
this.startServer();
|
|
}
|
|
|
|
async stop() {
|
|
await this.killClient();
|
|
await this.killServer();
|
|
await sleep(2000);
|
|
}
|
|
}
|
|
|
|
export default AutheliaServerWithHotReload; |