Skip to content

Commit

Permalink
Fix extension loading #784, #847, fixes #855
Browse files Browse the repository at this point in the history
  • Loading branch information
bpatrik committed Mar 25, 2024
1 parent f551509 commit d4d8dcf
Show file tree
Hide file tree
Showing 21 changed files with 300 additions and 176 deletions.
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pigallery2-extension-kit",
"version": "2.0.3-edge4",
"version": "2.0.3-edge6",
"description": "Interfaces for developing extensions for pigallery2",
"author": "Patrik J. Braun",
"homepage": "https://github.com/bpatrik/pigallery2",
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as child_process from 'child_process';
// @ts-ignore
import * as jeditor from 'gulp-json-editor';
import {XLIFF} from 'xlf-google-translate';
import {PrivateConfigClass} from './src/common/config/private/Config';
import {PrivateConfigClass} from './src/common/config/private/PrivateConfigClass';
import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder';

const execPr = util.promisify(child_process.exec);
Expand Down
2 changes: 1 addition & 1 deletion src/backend/middlewares/RenderingMWs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
import {Message} from '../../common/entities/Message';
import {Config, PrivateConfigClass} from '../../common/config/private/Config';
import {PrivateConfigClass} from '../../common/config/private/PrivateConfigClass';
import {UserDTO, UserRoles} from '../../common/entities/UserDTO';
import {NotificationManager} from '../model/NotifocationManager';
import {Logger} from '../Logger';
Expand Down
4 changes: 2 additions & 2 deletions src/backend/middlewares/admin/SettingsMWs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export class SettingsMWs {
await ConfigDiagnostics.runDiagnostics();
// restart all schedule timers. In case they have changed
ObjectManagers.getInstance().JobManager.runSchedules();
Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
// Logger.info(LOG_TAG, 'new config:');
// Logger.info(LOG_TAG, JSON.stringify(Config.toJSON({attachDescription: false}), null, '\t'));
return next();
} catch (err) {
if (err instanceof Error) {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/model/diagnostics/ConfigDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Config, PrivateConfigClass} from '../../../common/config/private/Config';
import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {Logger} from '../../Logger';
import {NotificationManager} from '../NotifocationManager';
import {SQLConnection} from '../database/SQLConnection';
Expand Down Expand Up @@ -27,8 +27,8 @@ import {SearchQueryParser} from '../../../common/SearchQueryParser';
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../common/Utils';
import {JobRepository} from '../jobs/JobRepository';
import {ExtensionConfig} from '../extension/ExtensionConfigWrapper';
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
import { Config } from '../../../common/config/private/Config';

const LOG_TAG = '[ConfigDiagnostics]';

Expand Down
18 changes: 18 additions & 0 deletions src/backend/model/extension/ExtensionConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {IExtensionConfig} from './IExtension';
import {Config} from '../../../common/config/private/Config';

export class ExtensionConfig<C> implements IExtensionConfig<C> {

constructor(private readonly extensionFolder: string) {
}


public getConfig(): C {
const c = (Config.Extensions.extensions || [])
.find(e => e.path === this.extensionFolder);

return c?.configs as C;
}


}
110 changes: 110 additions & 0 deletions src/backend/model/extension/ExtensionConfigTemplateLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import * as fs from 'fs';
import * as path from 'path';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';


const LOG_TAG = '[ExtensionConfigTemplateLoader]';

/**
* This class decouples the extension management and the config.
* It helps to solve the "chicken and the egg" which should load first:
* Config or the extension as they have a circular dependency
*/
export class ExtensionConfigTemplateLoader {

private static instance: ExtensionConfigTemplateLoader;
private extensionsFolder: string;

private loaded = false;
private extensionList: string[] = [];
private extensionTemplates: { folder: string, template?: { new(): unknown } }[] = [];

public static get Instance() {
if (!this.instance) {
this.instance = new ExtensionConfigTemplateLoader();
}

return this.instance;
}


init(extensionsFolder: string) {
this.extensionsFolder = extensionsFolder;
}

public loadExtensionTemplates(config: PrivateConfigClass) {
if (!this.extensionsFolder) {
throw new Error('Unknown extensions folder.');
}
// already loaded
if (!this.loaded) {

this.extensionList = (fs
.readdirSync(this.extensionsFolder))
.filter((f): boolean =>
fs.statSync(path.join(this.extensionsFolder, f)).isDirectory()
);
this.extensionList.sort();

this.extensionTemplates = [];
for (let i = 0; i < this.extensionList.length; ++i) {
const extFolder = this.extensionList[i];
const extPath = path.join(this.extensionsFolder, extFolder);
const serverExtPath = path.join(extPath, 'server.js');
if (!fs.existsSync(serverExtPath)) {
continue;
}


// eslint-disable-next-line @typescript-eslint/no-var-requires
const ext = require(serverExtPath);
if (typeof ext?.initConfig === 'function') {
ext?.initConfig({
setConfigTemplate: (template: { new(): unknown }): void => {
this.extensionTemplates.push({folder: extFolder, template: template});
}
});
} else {
//also create basic config extensions that do not have any
this.extensionTemplates.push({folder: extFolder});
}
}
this.loaded = true;
}

this.setTemplatesToConfig(config);
}




private setTemplatesToConfig(config: PrivateConfigClass) {
if (!this.extensionTemplates) {
return;
}

const ePaths = this.extensionTemplates.map(et => et.folder);
// delete not existing extensions
config.Extensions.extensions = config.Extensions.extensions
.filter(ec => ePaths.indexOf(ec.path) !== -1);


for (let i = 0; i < this.extensionTemplates.length; ++i) {
const ext = this.extensionTemplates[i];

let c = (config.Extensions.extensions || [])
.find(e => e.path === ext.folder);

// set the new structure with the new def values
if (!c) {
c = new ServerExtensionsEntryConfig(ext.folder);
if (ext.template) {
c.configs= new ext.template()
}
config.Extensions.extensions.push(c);
}

}
}
}
73 changes: 29 additions & 44 deletions src/backend/model/extension/ExtensionConfigWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,49 @@
import {IConfigClass} from 'typeconfig/common';
import {Config, PrivateConfigClass} from '../../../common/config/private/Config';
import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {ConfigClassBuilder} from 'typeconfig/node';
import {IExtensionConfig} from './IExtension';
import {ObjectManagers} from '../ObjectManagers';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
import {ExtensionConfigTemplateLoader} from './ExtensionConfigTemplateLoader';
import {NotificationManager} from '../NotifocationManager';


const LOG_TAG = '[ExtensionConfigWrapper]';

/**
* Wraps to original config and makes sure all extension related config is loaded
*/
export class ExtensionConfigWrapper {
static async original(): Promise<PrivateConfigClass & IConfigClass> {

static async original(showError = false): Promise<PrivateConfigClass & IConfigClass> {
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
ExtensionConfigTemplateLoader.Instance.loadExtensionTemplates(pc);
try {
await pc.load(); // loading the basic configs but we do not know the extension config hierarchy yet
if (ObjectManagers.isReady()) {
for (const ext of Object.values(ObjectManagers.getInstance().ExtensionManager.extObjects)) {
ext.config.loadToConfig(ConfigClassBuilder.attachPrivateInterface(pc));
}
}
await pc.load(); // loading the extension related configs
await pc.load(); // loading the basic configs, but we do not know the extension config hierarchy yet

} catch (e) {
console.error('Error during loading original config. Reverting to defaults.');
console.error(e);
if(showError){
console.error(LOG_TAG,'Error during loading config. Reverting to defaults.');
console.error(LOG_TAG,'This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.');
console.error(e);
NotificationManager.error('Can\'t load config. Reverting to default. This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.', (e.toString ? e.toString() : JSON.stringify(e)));
}
}
return pc;
}
}

export class ExtensionConfig<C> implements IExtensionConfig<C> {
public template: new() => C;

constructor(private readonly extensionFolder: string) {
}
static originalSync(showError = false): PrivateConfigClass & IConfigClass {
const pc = ConfigClassBuilder.attachPrivateInterface(new PrivateConfigClass());
ExtensionConfigTemplateLoader.Instance.loadExtensionTemplates(pc);
try {
pc.loadSync(); // loading the basic configs, but we do not know the extension config hierarchy yet

private findConfig(config: PrivateConfigClass): ServerExtensionsEntryConfig {
let c = (config.Extensions.extensions || []).find(e => e.path === this.extensionFolder);
if (!c) {
c = new ServerExtensionsEntryConfig(this.extensionFolder);
config.Extensions.extensions.push(c);
} catch (e) {
if(showError){
console.error(LOG_TAG,'Error during loading config. Reverting to defaults.');
console.error(LOG_TAG,'This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.');
console.error(e);
NotificationManager.error('Ca\'nt load config. Reverting to default. This is most likely due to: 1) you added a bad configuration in the server.json OR 2) The configuration changed in the latest release.', (e.toString ? e.toString() : JSON.stringify(e)));
}
return c;

}

public getConfig(): C {
return this.findConfig(Config).configs as C;
}

public setTemplate(template: new() => C): void {
this.template = template;
this.loadToConfig(Config);
}

loadToConfig(config: PrivateConfigClass) {
if (!this.template) {
return;
}

const confTemplate = ConfigClassBuilder.attachPrivateInterface(new this.template());
const extConf = this.findConfig(config);
extConf.configs = confTemplate;
return pc;
}
}
12 changes: 2 additions & 10 deletions src/backend/model/extension/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {SQLConnection} from '../database/SQLConnection';
import {ExtensionObject} from './ExtensionObject';
import {ExtensionDecoratorObject} from './ExtensionDecorator';
import * as util from 'util';
import {ServerExtensionsEntryConfig} from '../../../common/config/private/subconfigs/ServerExtensionsConfig';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = util.promisify(require('child_process').exec);

Expand Down Expand Up @@ -80,15 +79,8 @@ export class ExtensionManager implements IObjectManager {
);
extList.sort();

// delete not existing extensions
Config.Extensions.extensions = Config.Extensions.extensions.filter(ec => extList.indexOf(ec.path) !== -1);

// Add new extensions
const ePaths = Config.Extensions.extensions.map(ec => ec.path);
extList.filter(ep => ePaths.indexOf(ep) === -1).forEach(ep =>
Config.Extensions.extensions.push(new ServerExtensionsEntryConfig(ep)));

Logger.debug(LOG_TAG, 'Extensions found ', JSON.stringify(Config.Extensions.extensions.map(ec => ec.path)));
Logger.debug(LOG_TAG, 'Extensions found: ', JSON.stringify(Config.Extensions.extensions.map(ec => ec.path)));
}

private createUniqueExtensionObject(name: string, folder: string): IExtensionObject<unknown> {
Expand All @@ -111,7 +103,7 @@ export class ExtensionManager implements IObjectManager {
const extFolder = Config.Extensions.extensions[i].path;
let extName = extFolder;

if(Config.Extensions.extensions[i].enabled === false){
if (Config.Extensions.extensions[i].enabled === false) {
Logger.silly(LOG_TAG, `Skipping ${extFolder} initiation. Extension is disabled.`);
}
const extPath = path.join(ProjectPath.ExtensionFolder, extFolder);
Expand Down
2 changes: 1 addition & 1 deletion src/backend/model/extension/ExtensionObject.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {IExtensionEvents, IExtensionObject} from './IExtension';
import {ExtensionApp} from './ExtensionApp';
import {ExtensionConfig} from './ExtensionConfigWrapper';
import {ExtensionDB} from './ExtensionDB';
import {ProjectPath} from '../../ProjectPath';
import {ExpressRouterWrapper} from './ExpressRouterWrapper';
import {createLoggerWrapper} from '../../Logger';
import * as express from 'express';
import {ExtensionMessengerHandler} from './ExtensionMessengerHandler';
import {ExtensionConfig} from './ExtensionConfig';

export class ExtensionObject<C> implements IExtensionObject<C> {

Expand Down
18 changes: 15 additions & 3 deletions src/backend/model/extension/IExtension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as express from 'express';
import {NextFunction, Request, Response} from 'express';
import {PrivateConfigClass} from '../../../common/config/private/Config';
import {PrivateConfigClass} from '../../../common/config/private/PrivateConfigClass';
import {ObjectManagers} from '../ObjectManagers';
import {ProjectPathClass} from '../../ProjectPath';
import {ILogger} from '../../Logger';
Expand Down Expand Up @@ -141,8 +141,6 @@ export interface IExtensionDB {
}

export interface IExtensionConfig<C> {
setTemplate(template: new() => C): void;

getConfig(): C;
}

Expand Down Expand Up @@ -210,11 +208,25 @@ export interface IExtensionObject<C = void> {
messengers: IExtensionMessengers;
}

export interface IExtensionConfigInit<C> {
/**
* Sets the config tempalte class
* @param template
*/
setConfigTemplate(template: new() => C): void;
}

/**
* Extension interface. All extension is expected to implement and export these methods
*/
export interface IServerExtension<C> {

/**
* This function can be called any time. It should only set the config template class
* @param extension
*/
initConfig(extension: IExtensionConfigInit<C>): void;

/**
* Extension init function. Extension should at minimum expose this function.
* @param extension
Expand Down
Loading

0 comments on commit d4d8dcf

Please sign in to comment.