refactor: Delete a lot of unused and duplicate code in Server and WebhookServer (#5080)

* store n8n version string in a const and use that everywhere

* reduce code duplication between Server and WebhookServer

* unify redis checks

* fix linting
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-01-04 11:38:48 +01:00
committed by GitHub
parent b67f803cbe
commit 8b19fdd5f0
25 changed files with 882 additions and 1324 deletions

View File

@@ -17,7 +17,7 @@ import replaceStream from 'replacestream';
import { promisify } from 'util';
import glob from 'fast-glob';
import { IDataObject, LoggerProxy, sleep } from 'n8n-workflow';
import { LoggerProxy, sleep } from 'n8n-workflow';
import { createHash } from 'crypto';
import config from '@/config';
@@ -234,314 +234,231 @@ export class Start extends Command {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { flags } = this.parse(Start);
// Wrap that the process does not close but we can still use async
await (async () => {
try {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch((error: Error) => {
logger.error(`There was an error initializing DB: "${error.message}"`);
processExitCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.exit(1);
});
// Make sure the settings exist
const userSettings = await UserSettings.prepareUserSettings();
if (!config.getEnv('userManagement.jwtSecret')) {
// If we don't have a JWT secret set, generate
// one based and save to config.
const encryptionKey = await UserSettings.getEncryptionKey();
// For a key off every other letter from encryption key
// CAREFUL: do not change this or it breaks all existing tokens.
let baseKey = '';
for (let i = 0; i < encryptionKey.length; i += 2) {
baseKey += encryptionKey[i];
}
config.set(
'userManagement.jwtSecret',
createHash('sha256').update(baseKey).digest('hex'),
);
}
if (!config.getEnv('endpoints.disableUi')) {
await Start.generateStaticAssets();
}
// Load all node and credential types
const loadNodesAndCredentials = LoadNodesAndCredentials();
await loadNodesAndCredentials.init();
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
// Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(loadNodesAndCredentials);
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
// Load the credentials overwrites if any exist
await CredentialsOverwrites(credentialTypes).init();
await loadNodesAndCredentials.generateTypesForFrontend();
// Wait till the database is ready
await startDbInitPromise;
const installedPackages = await getAllInstalledPackages();
const missingPackages = new Set<{
packageName: string;
version: string;
}>();
installedPackages.forEach((installedPackage) => {
installedPackage.installedNodes.forEach((installedNode) => {
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) {
// Leave the list ready for installing in case we need.
missingPackages.add({
packageName: installedPackage.packageName,
version: installedPackage.installedVersion,
});
}
});
});
await UserSettings.getEncryptionKey();
// Load settings from database and set them to config.
const databaseSettings = await Db.collections.Settings.find({ loadOnStartup: true });
databaseSettings.forEach((setting) => {
config.set(setting.key, JSON.parse(setting.value));
});
config.set('nodes.packagesMissing', '');
if (missingPackages.size) {
LoggerProxy.error(
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
);
if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) {
LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages });
try {
// Optimistic approach - stop if any installation fails
// eslint-disable-next-line no-restricted-syntax
for (const missingPackage of missingPackages) {
// eslint-disable-next-line no-await-in-loop
void (await loadNodesAndCredentials.loadNpmModule(
missingPackage.packageName,
missingPackage.version,
));
missingPackages.delete(missingPackage);
}
LoggerProxy.info(
'Packages reinstalled successfully. Resuming regular initialization.',
);
} catch (error) {
LoggerProxy.error('n8n was unable to install the missing packages.');
}
}
}
if (missingPackages.size) {
config.set(
'nodes.packagesMissing',
Array.from(missingPackages)
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
.join(' '),
);
}
if (config.getEnv('executions.mode') === 'queue') {
const redisHost = config.getEnv('queue.bull.redis.host');
const redisUsername = config.getEnv('queue.bull.redis.username');
const redisPassword = config.getEnv('queue.bull.redis.password');
const redisPort = config.getEnv('queue.bull.redis.port');
const redisDB = config.getEnv('queue.bull.redis.db');
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
let lastTimer = 0;
let cumulativeTimeout = 0;
const settings = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
retryStrategy: (times: number): number | null => {
const now = Date.now();
if (now - lastTimer > 30000) {
// Means we had no timeout at all or last timeout was temporary and we recovered
lastTimer = now;
cumulativeTimeout = 0;
} else {
cumulativeTimeout += now - lastTimer;
lastTimer = now;
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
);
process.exit(1);
}
}
return 500;
},
} as IDataObject;
if (redisHost) {
settings.host = redisHost;
}
if (redisUsername) {
settings.username = redisUsername;
}
if (redisPassword) {
settings.password = redisPassword;
}
if (redisPort) {
settings.port = redisPort;
}
if (redisDB) {
settings.db = redisDB;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const { default: Redis } = await import('ioredis');
// This connection is going to be our heartbeat
// IORedis automatically pings redis and tries to reconnect
// We will be using the retryStrategy above
// to control how and when to exit.
const redis = new Redis(settings);
redis.on('error', (error) => {
if (error.toString().includes('ECONNREFUSED') === true) {
logger.warn('Redis unavailable - trying to reconnect...');
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
logger.warn('Error with Redis: ', error);
}
});
}
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
if (dbType === 'sqlite') {
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
if (shouldRunVacuum) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await Db.collections.Execution.query('VACUUM;');
}
}
if (flags.tunnel) {
this.log('\nWaiting for tunnel ...');
let tunnelSubdomain;
if (
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
) {
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
} else if (userSettings.tunnelSubdomain !== undefined) {
tunnelSubdomain = userSettings.tunnelSubdomain;
}
if (tunnelSubdomain === undefined) {
// When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
userSettings.tunnelSubdomain = Array.from({ length: 24 })
.map(() => {
return availableCharacters.charAt(
Math.floor(Math.random() * availableCharacters.length),
);
})
.join('');
await UserSettings.writeUserSettings(userSettings);
}
const tunnelSettings: localtunnel.TunnelConfig = {
host: 'https://hooks.n8n.cloud',
subdomain: tunnelSubdomain,
};
const port = config.getEnv('port');
// @ts-ignore
const webhookTunnel = await localtunnel(port, tunnelSettings);
process.env.WEBHOOK_URL = `${webhookTunnel.url}/`;
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
this.log(
'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!',
);
}
const instanceId = await UserSettings.getInstanceId();
const { cli } = await GenericHelpers.getVersions();
await InternalHooksManager.init(instanceId, cli, nodeTypes);
const binaryDataConfig = config.getEnv('binaryDataManager');
await BinaryDataManager.init(binaryDataConfig, true);
await Server.start();
// Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.init();
WaitTracker();
const editorUrl = GenericHelpers.getBaseUrl();
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
if (saveManualExecutions) {
this.log('\nManual executions will be visible only for the owner');
}
// Allow to open n8n editor by pressing "o"
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let inputText = '';
if (flags.open) {
Start.openBrowser();
}
this.log('\nPress "o" to open in Browser.');
process.stdin.on('data', (key: string) => {
if (key === 'o') {
Start.openBrowser();
inputText = '';
} else if (key.charCodeAt(0) === 3) {
// Ctrl + c got pressed
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Start.stopProcess();
} else {
// When anything else got pressed, record it and send it on enter into the child process
// eslint-disable-next-line no-lonely-if
if (key.charCodeAt(0) === 13) {
// send to child process and print in terminal
process.stdout.write('\n');
inputText = '';
} else {
// record it and write into terminal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inputText += key;
process.stdout.write(key);
}
}
});
}
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
this.error(`There was an error: ${error.message}`);
try {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch((error: Error) => {
logger.error(`There was an error initializing DB: "${error.message}"`);
processExitCode = 1;
// @ts-ignore
process.emit('SIGINT');
process.emit('exit', processExitCode);
});
// Make sure the settings exist
const userSettings = await UserSettings.prepareUserSettings();
if (!config.getEnv('userManagement.jwtSecret')) {
// If we don't have a JWT secret set, generate
// one based and save to config.
const encryptionKey = await UserSettings.getEncryptionKey();
// For a key off every other letter from encryption key
// CAREFUL: do not change this or it breaks all existing tokens.
let baseKey = '';
for (let i = 0; i < encryptionKey.length; i += 2) {
baseKey += encryptionKey[i];
}
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
}
})();
if (!config.getEnv('endpoints.disableUi')) {
await Start.generateStaticAssets();
}
// Load all node and credential types
const loadNodesAndCredentials = LoadNodesAndCredentials();
await loadNodesAndCredentials.init();
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
// Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(loadNodesAndCredentials);
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
// Load the credentials overwrites if any exist
await CredentialsOverwrites(credentialTypes).init();
await loadNodesAndCredentials.generateTypesForFrontend();
// Wait till the database is ready
await startDbInitPromise;
const installedPackages = await getAllInstalledPackages();
const missingPackages = new Set<{
packageName: string;
version: string;
}>();
installedPackages.forEach((installedPackage) => {
installedPackage.installedNodes.forEach((installedNode) => {
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) {
// Leave the list ready for installing in case we need.
missingPackages.add({
packageName: installedPackage.packageName,
version: installedPackage.installedVersion,
});
}
});
});
await UserSettings.getEncryptionKey();
// Load settings from database and set them to config.
const databaseSettings = await Db.collections.Settings.find({ loadOnStartup: true });
databaseSettings.forEach((setting) => {
config.set(setting.key, JSON.parse(setting.value));
});
config.set('nodes.packagesMissing', '');
if (missingPackages.size) {
LoggerProxy.error(
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
);
if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) {
LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages });
try {
// Optimistic approach - stop if any installation fails
// eslint-disable-next-line no-restricted-syntax
for (const missingPackage of missingPackages) {
// eslint-disable-next-line no-await-in-loop
void (await loadNodesAndCredentials.loadNpmModule(
missingPackage.packageName,
missingPackage.version,
));
missingPackages.delete(missingPackage);
}
LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.');
} catch (error) {
LoggerProxy.error('n8n was unable to install the missing packages.');
}
}
config.set(
'nodes.packagesMissing',
Array.from(missingPackages)
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
.join(' '),
);
}
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
if (dbType === 'sqlite') {
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
if (shouldRunVacuum) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await Db.collections.Execution.query('VACUUM;');
}
}
if (flags.tunnel) {
this.log('\nWaiting for tunnel ...');
let tunnelSubdomain;
if (
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
) {
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
} else if (userSettings.tunnelSubdomain !== undefined) {
tunnelSubdomain = userSettings.tunnelSubdomain;
}
if (tunnelSubdomain === undefined) {
// When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
userSettings.tunnelSubdomain = Array.from({ length: 24 })
.map(() => {
return availableCharacters.charAt(
Math.floor(Math.random() * availableCharacters.length),
);
})
.join('');
await UserSettings.writeUserSettings(userSettings);
}
const tunnelSettings: localtunnel.TunnelConfig = {
host: 'https://hooks.n8n.cloud',
subdomain: tunnelSubdomain,
};
const port = config.getEnv('port');
// @ts-ignore
const webhookTunnel = await localtunnel(port, tunnelSettings);
process.env.WEBHOOK_URL = `${webhookTunnel.url}/`;
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
this.log(
'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!',
);
}
const instanceId = await UserSettings.getInstanceId();
await InternalHooksManager.init(instanceId, nodeTypes);
const binaryDataConfig = config.getEnv('binaryDataManager');
await BinaryDataManager.init(binaryDataConfig, true);
await Server.start();
// Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.init();
WaitTracker();
const editorUrl = GenericHelpers.getBaseUrl();
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
if (saveManualExecutions) {
this.log('\nManual executions will be visible only for the owner');
}
// Allow to open n8n editor by pressing "o"
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let inputText = '';
if (flags.open) {
Start.openBrowser();
}
this.log('\nPress "o" to open in Browser.');
process.stdin.on('data', (key: string) => {
if (key === 'o') {
Start.openBrowser();
inputText = '';
} else if (key.charCodeAt(0) === 3) {
// Ctrl + c got pressed
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Start.stopProcess();
} else {
// When anything else got pressed, record it and send it on enter into the child process
// eslint-disable-next-line no-lonely-if
if (key.charCodeAt(0) === 13) {
// send to child process and print in terminal
process.stdout.write('\n');
inputText = '';
} else {
// record it and write into terminal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inputText += key;
process.stdout.write(key);
}
}
});
}
} catch (error) {
console.error('There was an error', error);
processExitCode = 1;
process.emit('exit', processExitCode);
}
}
}