evergreen/distribution/client/src/client.ts

193 lines
6.1 KiB
TypeScript

/*
* This is the main entrypoint for the evergreen-client
*/
import feathers from '@feathersjs/feathers';
import * as logger from 'winston';
import socketio from '@feathersjs/socketio-client';
import auth from '@feathersjs/authentication-client';
import io from 'socket.io-client';
import ErrorTelemetry from './lib/error-telemetry';
import HealthChecker from './lib/healthchecker';
import Registration from './lib/registration';
import Status from './lib/status';
import Storage from './lib/storage';
import UI from './lib/ui'
import Update from './lib/update';
import Periodic from './lib/periodic';
/*
* The Client class is a simple wrapper meant to start the basics of the client
* and then run a simple runloop to block the client from ever exiting
*/
export default class Client {
protected readonly app : any;
protected readonly reg : Registration;
protected readonly healthChecker : HealthChecker;
protected readonly update : Update;
protected readonly status : Status;
protected readonly errorTelemetry : ErrorTelemetry;
protected socket : any;
public updating : boolean;
constructor() {
if (!process.env.FLAVOR) {
logger.error('Fatal error encountered while trying to start, no flavor set, exiting the client');
throw new Error('Missing flavor definition');
}
this.app = feathers();
this.reg = new Registration(this.app);
this.healthChecker = new HealthChecker(process.env.JENKINS_URL || 'http://127.0.0.1:8080');
this.update = new Update(this.app, { healthChecker: this.healthChecker });
this.status = new Status(this.app, { flavor: process.env.FLAVOR });
this.errorTelemetry = new ErrorTelemetry(this.app, { flavor: process.env.FLAVOR });
this.updating = false;
// This should be overridden on bootstrap
this.socket = null;
}
/*
* Determine whether the instance should be considered offline or not
*
* @return {boolean} Defaults to false unless EVERGREEN_OFFLINE=1 is et in
* the environment
*/
isOffline() {
return !!process.env.EVERGREEN_OFFLINE;
}
async runUpdates() {
if (this.isOffline()) {
logger.info('Evergreen in offline mode, disabling downloading of updates..');
return false;
}
UI.publish(`Checking for updates from ${process.env.EVERGREEN_ENDPOINT}`);
return this.update.query()
.then(updates => this.update.applyUpdates(updates))
.then(() => this.status.reportVersions())
.then(() => this.status.reportLevel(this.update.getCurrentLevel()))
.catch((err) => {
if (err.type == 'invalid-json') {
logger.warn('Received non-JSON response from the Update service');
} else if (err.code == 304) {
logger.debug('No updates available at this time');
} else {
UI.publish('Failed to query for updates!', { log: 'error', error: err });
}
});
}
runloop(app) {
logger.info('..starting runloop');
/*
* Only setting on the cron once we have registered and logged in,
* otherwise it's not really useful to have anything running periodically
*/
const cron = new Periodic();
this.runUpdates();
this.healthChecker.check().then((state) => {
if (state.healthy) {
UI.publish('Jenkins appears to be online', { log: 'info' });
Storage.removeBootingFlag();
} else {
UI.publish('Jenkins appears to be in an unhealthy state!', { log: 'error' });
}
});
cron.runDaily('post-status', () => {
this.status.reportVersions();
});
cron.runHourly('check-for-updates', () => {
this.runUpdates();
});
try {
this.errorTelemetry.setup();
} catch (err) {
logger.error('Failed to set up Error Telemetry, treating as non-fatal for now', err);
}
setInterval(() => {
/* keep this process alive */
this.tick();
}, (5 * (60 * 1000)));
}
tick() {
const level = this.update.getCurrentLevel();
this.status.reportLevel(level);
}
bootstrap() {
const endpoint = process.env.EVERGREEN_ENDPOINT;
logger.info('Configuring the client to use the endpoint %s', endpoint);
this.socket = io(endpoint, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax : 5000,
reconnectionAttempts: Infinity
});
this.app.configure(socketio(this.socket));
this.app.configure(auth({
}));
this.app.on('reauthentication-error', (error) => {
logger.info('Client must re-authenticate..', error);
return this.reg.login();
});
logger.info('Registering listener for event: `update created`');
this.app.service('update').on('created', (message) => {
logger.info('Received an Update `created` event, checking for updates', message);
this.runUpdates();
});
logger.info('Registering listener for event: `status ping`');
this.app.service('status').on('ping', (message) => {
logger.debug('Received ping', message);
});
this.socket.on('reconnect', () => {
logger.info('Reconnecting the socket.io channel, and checking for updates');
this.runUpdates();
});
this.reg.register().then(({res, newRegistration}) => {
UI.publish('Registered this Evergreen instance', { log: 'debug', error: res} );
this.status.authenticate(this.reg.uuid, this.reg.token);
this.update.authenticate(this.reg.uuid, this.reg.token);
this.errorTelemetry.authenticate(this.reg.uuid, this.reg.token);
return this.status.create().then((r) => {
logger.info('Starting the runloop with a new registration and status', r, newRegistration);
this.runloop(this.app);
});
}).catch((err) => {
UI.publish('Fatal error encountered while trying to register, exiting the client and will restart and retry', { log: 'error', error: err });
process.exit(1);
});
}
}
if (require.main === module) {
Storage.setBootingFlag();
UI.serve();
/*
* Allow the log level to be overridden in the environment for debugging
* purposes by the user
*/
logger.level = process.env.LOG_LEVEL || 'warn';
/* Main entrypoint for module */
UI.publish('Starting the evergreen-client..', { log: 'info' });
const client = new Client();
client.bootstrap();
}