diff --git a/packages/plugin/src/decorators.ts b/packages/plugin/src/decorators.ts index a7346d07..e1ebdfd6 100644 --- a/packages/plugin/src/decorators.ts +++ b/packages/plugin/src/decorators.ts @@ -1,3 +1,4 @@ +import { plugin as pluginApi } from "@ccms/api" import { injectable, decorate } from "@ccms/container" import { interfaces } from './interfaces' import { METADATA_KEY } from './constants' @@ -7,16 +8,16 @@ import { getPluginMetadatas, getPluginCommandMetadata, getPluginListenerMetadata * MiaoScript plugin * @param metadata PluginMetadata */ -export function plugin(metadata: interfaces.PluginMetadata) { +export function plugin(metadata: pluginApi.PluginMetadata) { return function (target: any) { metadata.target = target metadata.type = "ioc" decorate(injectable(), target) Reflect.defineMetadata(METADATA_KEY.plugin, metadata, target) - const previousMetadata: Map = getPluginMetadatas() + const previousMetadata: Map = getPluginMetadatas() previousMetadata.set(metadata.name, metadata) Reflect.defineMetadata(METADATA_KEY.plugin, previousMetadata, Reflect) - const previousSources: Map = getPluginSources() + const previousSources: Map = getPluginSources() previousSources.set(metadata.source.toString(), metadata) Reflect.defineMetadata(METADATA_KEY.souece, previousSources, Reflect) } diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 8b2e85a2..a0122158 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -1,3 +1,15 @@ +import './scanner/file-scanner' +import './loader/ioc-loader' +import './loader/basic-loader' + export * from './manager' export * from './decorators' export * from './interfaces' + +export { + plugin as JSPlugin, + cmd as Cmd, + tab as Tab, + listener as Listener, + config as Config +} from './decorators' diff --git a/packages/plugin/src/interfaces.ts b/packages/plugin/src/interfaces.ts index 02e2ff3f..7e0e5a6b 100644 --- a/packages/plugin/src/interfaces.ts +++ b/packages/plugin/src/interfaces.ts @@ -1,14 +1,14 @@ -import { server, MiaoScriptConsole, event, plugin } from "@ccms/api"; -import { injectable, inject, postConstruct } from "@ccms/container"; -import { getPluginMetadata } from "./utils"; +import { server, MiaoScriptConsole, event, plugin } from "@ccms/api" +import { injectable, inject, postConstruct } from "@ccms/container" +import { getPluginMetadata } from "./utils" export namespace interfaces { @injectable() export abstract class Plugin implements plugin.Plugin { - public description: PluginMetadata; - public logger: Console; + public description: plugin.PluginMetadata + public logger: Console @inject(server.Console) - private Console: MiaoScriptConsole; + private Console: MiaoScriptConsole constructor() { this.description = getPluginMetadata(this) @@ -24,86 +24,45 @@ export namespace interfaces { public enable() { } public disable() { } } - interface BaseMetadata { - /** - * 名称 为空则为对象名称 - */ - name?: string; - /** - * 支持的服务器列表 为空则代表所有 - */ - servers?: string[]; - } - export interface PluginMetadata extends BaseMetadata { - /** - * 插件名称 - */ - name: string; - /** - * 前缀 - */ - prefix?: string; - /** - * 插件版本 - */ - version: string; - /** - * 插件版本 - */ - author: string | string[]; - /** - * 插件源文件 必须指定为 __filename - */ - source: string; - /** - * 插件类型 默认为 ioc 执行 MiaoScript 加载逻辑 - */ - type?: string; - /** - * 插件本体 - */ - target?: any; - } - export interface ExecMetadata extends BaseMetadata { + export interface ExecMetadata extends plugin.BaseMetadata { /** * 执行器 */ - executor?: string; + executor?: string } export interface CommandMetadata extends ExecMetadata { /** * 参数列表 */ - paramtypes?: string[]; + paramtypes?: string[] } export interface ListenerMetadata extends ExecMetadata { /** * 监听优先级 */ - priority?: event.EventPriority; + priority?: event.EventPriority /** * 是否忽略已取消的事件 */ - ignoreCancel?: boolean; - + ignoreCancel?: boolean } - export interface ConfigMetadata extends BaseMetadata { + export interface ConfigMetadata extends plugin.BaseMetadata { /** * 配置文件版本号 */ - version?: number; + version?: number /** * 实体变量名称 */ - variable?: string; + variable?: string /** * 配置文件格式 默认 yml */ - format?: string; + format?: string /** * 是否为只读(关闭时将不会自动保存) */ - readonly?: boolean; + readonly?: boolean } - export type PluginLike = Plugin | string; + export type PluginLike = Plugin | string } diff --git a/packages/plugin/src/loader/basic-loader.ts b/packages/plugin/src/loader/basic-loader.ts new file mode 100644 index 00000000..33863174 --- /dev/null +++ b/packages/plugin/src/loader/basic-loader.ts @@ -0,0 +1,24 @@ +import { plugin } from "@ccms/api" +import { provideSingleton } from "@ccms/container" + +@provideSingleton(plugin.PluginLoader) +export class BasicLoader implements plugin.PluginLoader { + type: string = 'basic' + + private pluginRequireMap: Map + + constructor() { + this.pluginRequireMap = new Map() + } + require(target: any, result: any) { + this.pluginRequireMap.set(target.toString(), result) + return result + } + build(metadata: plugin.PluginMetadata) { + return this.pluginRequireMap.get(metadata.source.toString()) + } + load(plugin: plugin.Plugin): void { } + enable(plugin: plugin.Plugin): void { } + disable(plugin: plugin.Plugin): void { } + reload(plugin: plugin.Plugin): void { } +} diff --git a/packages/plugin/src/loader/ioc-loader.ts b/packages/plugin/src/loader/ioc-loader.ts new file mode 100644 index 00000000..38795d9e --- /dev/null +++ b/packages/plugin/src/loader/ioc-loader.ts @@ -0,0 +1,97 @@ +import { plugin, server } from "@ccms/api" +import { inject, ContainerInstance, Container, provideSingleton } from "@ccms/container" + +import { interfaces } from "../interfaces" +import { getPluginStageMetadata, getPluginSources } from "../utils" + +@provideSingleton(plugin.PluginLoader) +export class IocLoader implements plugin.PluginLoader { + type: string = 'ioc' + @inject(ContainerInstance) + private container: Container + @inject(server.ServerType) + private serverType: string + + private pluginMetadataMap: Map + + constructor() { + this.pluginMetadataMap = getPluginSources() + } + + require(target: any, result: any) { + return this.pluginMetadataMap.get(target.toString()) + } + + build(metadata: plugin.PluginMetadata) { + if (!this.allowProcess(metadata.servers)) { return } + let pluginInstance: plugin.Plugin + try { + this.bindPlugin(metadata) + pluginInstance = this.container.getNamed(plugin.Plugin, metadata.name) + if (!(pluginInstance instanceof interfaces.Plugin)) { + console.i18n('ms.plugin.manager.build.not.extends', { source: metadata.source }) + return + } + } catch (ex) { + console.i18n("ms.plugin.manager.initialize.error", { name: metadata.name, ex }) + console.ex(ex) + } + return pluginInstance + } + + load(plugin: plugin.Plugin): void { + this.stage(plugin, 'load') + } + enable(plugin: plugin.Plugin): void { + this.stage(plugin, 'enable') + } + disable(plugin: plugin.Plugin): void { + this.stage(plugin, 'disable') + } + reload(plugin: plugin.Plugin): void { + this.disable(plugin) + //@ts-ignore + require(plugin.description.source, { cache: false }) + this.load(plugin) + this.enable(plugin) + } + + private bindPlugin(metadata: plugin.PluginMetadata) { + try { + let pluginInstance = this.container.getNamed(plugin.Plugin, metadata.name) + if (pluginInstance.description.source + '' !== metadata.source + '') { + console.i18n('ms.plugin.manager.build.duplicate', { exists: pluginInstance.description.source, source: metadata.source }) + } + this.container.rebind(plugin.Plugin).to(metadata.target).inSingletonScope().whenTargetNamed(metadata.name) + } catch{ + this.container.bind(plugin.Plugin).to(metadata.target).inSingletonScope().whenTargetNamed(metadata.name) + } + } + + private allowProcess(servers: string[]) { + // Not set servers -> allow + if (!servers || !servers.length) return true + // include !type -> deny + let denyServers = servers.filter(svr => svr.startsWith("!")) + if (denyServers.length !== 0) { + return !denyServers.includes(`!${this.serverType}`) + } else { + // only include -> allow + return servers.includes(this.serverType) + } + } + + private stage(pluginInstance: plugin.Plugin, stageName: string) { + let stages = getPluginStageMetadata(pluginInstance, stageName) + for (const stage of stages) { + if (!this.allowProcess(stage.servers)) { continue } + console.i18n("ms.plugin.manager.stage.exec", { plugin: pluginInstance.description.name, name: stage.executor, stage: stageName, servers: stage.servers }) + try { + pluginInstance[stage.executor].apply(pluginInstance) + } catch (error) { + console.i18n("ms.plugin.manager.stage.exec.error", { plugin: pluginInstance.description.name, executor: stage.executor, error }) + console.ex(error) + } + } + } +} \ No newline at end of file diff --git a/packages/plugin/src/manager.ts b/packages/plugin/src/manager.ts index 921d25be..090cf78f 100644 --- a/packages/plugin/src/manager.ts +++ b/packages/plugin/src/manager.ts @@ -3,9 +3,9 @@ import { plugin, server, command, event } from '@ccms/api' import { inject, provideSingleton, Container, ContainerInstance } from '@ccms/container' import * as fs from '@ccms/common/dist/fs' -import { getPluginMetadatas, getPluginCommandMetadata, getPluginListenerMetadata, getPlugin, getPluginTabCompleterMetadata, getPluginConfigMetadata, getPluginStageMetadata, getPluginSources } from './utils' import { interfaces } from './interfaces' import { getConfigLoader } from './config' +import { getPluginCommandMetadata, getPluginListenerMetadata, getPluginTabCompleterMetadata, getPluginConfigMetadata } from './utils' const Thread = Java.type('java.lang.Thread') @@ -25,27 +25,53 @@ export class PluginManagerImpl implements plugin.PluginManager { private EventManager: event.Event private initialized: boolean = false - private pluginRequireMap: Map - private pluginInstanceMap: Map - private pluginMetadataMap: Map + + private sacnnerMap: Map + private loaderMap: Map + + private instanceMap: Map + private metadataMap: Map + + constructor() { + this.sacnnerMap = new Map() + this.loaderMap = new Map() + + this.instanceMap = new Map() + this.metadataMap = new Map() + } initialize() { if (this.pluginInstance === undefined) { throw new Error("Can't found Plugin Instance!") } if (this.initialized !== true) { console.i18n('ms.plugin.initialize', { plugin: this.pluginInstance, loader: Thread.currentThread().contextClassLoader }) console.i18n('ms.plugin.event.map', { count: this.EventManager.mapEventName().toFixed(0), type: this.serverType }) - this.pluginRequireMap = new Map() - this.pluginInstanceMap = new Map() - this.pluginMetadataMap = getPluginSources() + let pluginScanner = this.container.getAll(plugin.PluginScanner) + pluginScanner.forEach((scanner) => { + console.debug(`loading plugin sacnner ${scanner.type}...`) + this.sacnnerMap.set(scanner.type, scanner) + }) + let pluginLoaders = this.container.getAll(plugin.PluginLoader) + pluginLoaders.forEach((loader) => { + console.debug(`loading plugin loader ${loader.type}...`) + this.loaderMap.set(loader.type, loader) + }) this.initialized = true } } scan(folder: string): void { + if (!folder) { throw new Error('plugin scan folder can\'t be empty!') } this.initialize() - var plugin = fs.file(root, folder) - var files = this.scanFolder(plugin) - this.loadPlugins(files) + for (const [, scanner] of this.sacnnerMap) { + try { + scanner.scan(folder).forEach(file => { + this.loadPlugin(file, scanner) + }) + } catch (error) { + console.error(`plugin scanner ${scanner.type} occurred error ${error}`) + console.ex(error) + } + } } build(): void { @@ -57,24 +83,45 @@ export class PluginManagerImpl implements plugin.PluginManager { } private runPluginStage(plugin: plugin.Plugin, stage: string, ext: Function) { + if (!plugin) { throw new Error(`can't run runPluginStage ${stage} because plugin is ${plugin}`) } try { this.logStage(plugin, i18n.translate(`ms.plugin.manager.stage.${stage}`)) ext() this.runCatch(plugin, stage) this.runCatch(plugin, `${this.serverType}${stage}`) - this.execPluginStage(plugin, stage) + plugin.description.loader[stage](plugin) } catch (ex) { console.i18n("ms.plugin.manager.stage.exec.error", { plugin: plugin.description.name, executor: stage, error: ex }) } } + private loadPlugin(file: string, scanner: plugin.PluginScanner) { + try { + let requireInstance = scanner.load(file) + for (const [, loader] of this.loaderMap) { + let metadata = loader.require(file, requireInstance) + if (metadata && metadata.source && metadata.name) { + metadata.loader = loader + this.metadataMap.set(metadata.name, metadata) + return metadata + } + } + } catch (error) { + console.i18n("ms.plugin.manager.initialize.error", { name: file, ex: error }) + console.ex(error) + } + console.console(`§efile §b${file} §ccan't load metadata. §eskip load!`) + } + /** * 从文件加载插件 * @param file java.io.File */ - loadFromFile(file: string): plugin.Plugin { - let metadata = this.loadPlugin(file) - let plugin = this.buildPlugin(metadata && metadata.description ? metadata.description : this.pluginMetadataMap.get(file.toString())) + loadFromFile(file: string, scanner = this.sacnnerMap.get('file')): plugin.Plugin { + if (!file) { throw new Error('plugin file can\'t be null!') } + if (!scanner) { throw new Error('plugin scanner can\'t be null!') } + let metadata = this.loadPlugin(file, scanner) + let plugin = metadata.loader.build(metadata) this.load(plugin) this.enable(plugin) return plugin @@ -110,16 +157,16 @@ export class PluginManagerImpl implements plugin.PluginManager { reload(...args: any[]): void { this.checkAndGet(args[0]).forEach((pl: plugin.Plugin) => { this.disable(pl) - this.loadFromFile(pl.description.source) + this.loadFromFile(pl.description.source, pl.description.scanner) }) } getPlugin(name: string) { - return this.pluginInstanceMap.get(name) + return this.instanceMap.get(name) } getPlugins() { - return this.pluginInstanceMap + return this.instanceMap } private runCatch(pl: any, func: string) { @@ -132,64 +179,13 @@ export class PluginManagerImpl implements plugin.PluginManager { } private checkAndGet(name: string | plugin.Plugin | undefined | any): Map | plugin.Plugin[] { - if (name == this.pluginInstanceMap) { return this.pluginInstanceMap } - if (typeof name == 'string' && this.pluginInstanceMap.has(name)) { return [this.pluginInstanceMap.get(name)] } + if (name == this.instanceMap) { return this.instanceMap } + if (typeof name == 'string' && this.instanceMap.has(name)) { return [this.instanceMap.get(name)] } if (name instanceof interfaces.Plugin) { return [name as plugin.Plugin] } if (name.description || name.description.name) { return [name as plugin.Plugin] } throw new Error(`Plugin ${JSON.stringify(name)} not exist!`) } - private scanFolder(folder: any): string[] { - var files = [] - console.i18n('ms.plugin.manager.scan', { folder }) - this.checkUpdateFolder(folder) - // must check file is exist maybe is a illegal symbolic link file - fs.list(folder).forEach((file: any) => file.toFile().exists() ? files.push(file.toFile()) : void 0) - return files - } - - /** - * 更新插件 - * @param path - */ - private checkUpdateFolder(path: any) { - var update = fs.file(path, "update") - if (!update.exists()) { - update.mkdirs() - } - } - - private loadPlugins(files: any[]): void { - this.loadJsPlugins(files) - } - - /** - * JS类型插件预加载 - */ - private loadJsPlugins(files: any[]) { - files.filter(file => file.name.endsWith(".js")).forEach(file => { - try { - this.loadPlugin(file) - } catch (ex) { - console.i18n("ms.plugin.manager.initialize.error", { name: file.name, ex }) - console.ex(ex) - } - }) - } - - private loadPlugin(file: any) { - this.updatePlugin(file) - return this.createPlugin(file.toString()) - } - - private updatePlugin(file: any) { - var update = fs.file(fs.file(file.parentFile, 'update'), file.name) - if (update.exists()) { - console.i18n("ms.plugin.manager.build.update", { name: file.name }) - fs.move(update, file, true) - } - } - private allowProcess(servers: string[]) { // Not set servers -> allow if (!servers || !servers.length) return true @@ -203,59 +199,14 @@ export class PluginManagerImpl implements plugin.PluginManager { } } - private createPlugin(file: string) { - //@ts-ignore - let instance = require(file, { cache: false }) - this.pluginRequireMap.set(file, instance) - return instance - } - private buildPlugins() { - let metadatas = [] - let pluginMetadatas = getPluginMetadatas() - for (const [_, metadata] of pluginMetadatas) { metadatas.push(metadata) } - for (const [_, instance] of this.pluginRequireMap) { if (instance.description) { this.buildPlugin(instance.description) } } - for (const metadata of metadatas) { - if (!this.allowProcess(metadata.servers)) { continue } - this.buildPlugin(metadata) - } - } - - private buildPlugin(metadata: interfaces.PluginMetadata) { - let pluginInstance: plugin.Plugin - switch (metadata.type) { - case "ioc": - try { - this.bindPlugin(metadata) - pluginInstance = this.container.getNamed(plugin.Plugin, metadata.name) - if (!(pluginInstance instanceof interfaces.Plugin)) { - console.i18n('ms.plugin.manager.build.not.extends', { source: metadata.source }) - return - } - } catch (ex) { - console.i18n("ms.plugin.manager.initialize.error", { name: metadata.name, ex }) - console.ex(ex) - } - break - case "basic": - pluginInstance = this.pluginRequireMap.get(metadata.source.toString()) - break - default: - throw new Error('§4不支持的插件类型 请检查加载器是否正常启用!') - } - pluginInstance && this.pluginInstanceMap.set(metadata.name, pluginInstance) - return pluginInstance - } - - private bindPlugin(metadata: interfaces.PluginMetadata) { - try { - let pluginInstance = this.container.getNamed(plugin.Plugin, metadata.name) - if (pluginInstance.description.source + '' !== metadata.source + '') { - console.i18n('ms.plugin.manager.build.duplicate', { exists: pluginInstance.description.source, source: metadata.source }) + for (const [, metadata] of this.metadataMap) { + let pluginInstance: plugin.Plugin + if (!this.loaderMap.has(metadata.type)) { + console.error(`§4无法加载插件 §c${metadata.name} §4请检查 §c${metadata.type} §4加载器是否正常启用!`) + continue } - this.container.rebind(plugin.Plugin).to(metadata.target).inSingletonScope().whenTargetNamed(metadata.name) - } catch{ - this.container.bind(plugin.Plugin).to(metadata.target).inSingletonScope().whenTargetNamed(metadata.name) + (pluginInstance = this.loaderMap.get(metadata.type).build(metadata)) && this.instanceMap.set(metadata.name, pluginInstance) } } @@ -331,18 +282,4 @@ export class PluginManagerImpl implements plugin.PluginManager { private unregistryListener(pluginInstance: plugin.Plugin) { this.EventManager.disable(pluginInstance) } - - private execPluginStage(pluginInstance: plugin.Plugin, stageName: string) { - let stages = getPluginStageMetadata(pluginInstance, stageName) - for (const stage of stages) { - if (!this.allowProcess(stage.servers)) { continue } - console.i18n("ms.plugin.manager.stage.exec", { plugin: pluginInstance.description.name, name: stage.executor, stage: stageName, servers: stage.servers }) - try { - pluginInstance[stage.executor].apply(pluginInstance) - } catch (error) { - console.i18n("ms.plugin.manager.stage.exec.error", { plugin: pluginInstance.description.name, executor: stage.executor, error }) - console.ex(error) - } - } - } } diff --git a/packages/plugin/src/scanner/file-scanner.ts b/packages/plugin/src/scanner/file-scanner.ts new file mode 100644 index 00000000..2c1a680d --- /dev/null +++ b/packages/plugin/src/scanner/file-scanner.ts @@ -0,0 +1,49 @@ +import { plugin } from "@ccms/api" +import * as fs from '@ccms/common/dist/fs' +import { provideSingletonNamed } from "@ccms/container" + +@provideSingletonNamed(plugin.PluginScanner, 'file') +export class JSFileScanner implements plugin.PluginScanner { + type: string = 'file' + + scan(target: any): string[] { + return this.scanFolder(fs.concat(root, target)) + } + + load(file: string) { + if (typeof file === "string") { return } + this.updatePlugin(file) + //@ts-ignore + return require(file.toString(), { cache: false }) + } + + private scanFolder(folder: any): string[] { + var files = [] + console.i18n('ms.plugin.manager.scan', { folder }) + this.checkUpdateFolder(folder) + // must check file is exist maybe is a illegal symbolic link file + fs.list(folder).forEach((path: any) => { + let file = path.toFile() + if (file.exists() && file.getName().endsWith(".js")) { + files.push(file) + } + }) + return files + } + + private checkUpdateFolder(path: any) { + var update = fs.file(path, "update") + if (!update.exists()) { + update.mkdirs() + } + } + + private updatePlugin(file: any) { + var update = fs.file(fs.file(file.parentFile, 'update'), file.name) + if (update.exists()) { + console.i18n("ms.plugin.manager.build.update", { name: file.name }) + fs.move(update, file, true) + } + } + +} diff --git a/packages/plugin/src/utils.ts b/packages/plugin/src/utils.ts index 3c444583..a322e9d9 100644 --- a/packages/plugin/src/utils.ts +++ b/packages/plugin/src/utils.ts @@ -1,78 +1,79 @@ +import { plugin } from '@ccms/api' import { interfaces } from './interfaces' import { METADATA_KEY } from './constants' -const pluginSourceCache = new Map(); +const pluginSourceCache = new Map() function getPlugins() { - return [...getPluginMetadatas().values()].map((target) => target.target); + return [...getPluginMetadatas().values()].map((target) => target.target) } function getPlugin(name: string) { - return getPluginMetadatas().get(name); + return getPluginMetadatas().get(name) } function getPluginSources() { - let pluginSources: Map = Reflect.getMetadata( + let pluginSources: Map = Reflect.getMetadata( METADATA_KEY.souece, Reflect - ) || pluginSourceCache; - return pluginSources; + ) || pluginSourceCache + return pluginSources } function getPluginMetadatas() { - let pluginMetadatas: Map = Reflect.getMetadata( + let pluginMetadatas: Map = Reflect.getMetadata( METADATA_KEY.plugin, Reflect - ) || new Map(); - return pluginMetadatas; + ) || new Map() + return pluginMetadatas } function getPluginMetadata(target: any) { - let pluginMetadata: interfaces.PluginMetadata = Reflect.getMetadata( + let pluginMetadata: plugin.PluginMetadata = Reflect.getMetadata( METADATA_KEY.plugin, target.constructor - ) || {}; - return pluginMetadata; + ) || {} + return pluginMetadata } function getPluginCommandMetadata(target: any) { let commandMetadata: Map = Reflect.getMetadata( METADATA_KEY.cmd, target.constructor - ) || new Map(); - return commandMetadata; + ) || new Map() + return commandMetadata } function getPluginTabCompleterMetadata(target: any) { let tabcompleterMetadata: Map = Reflect.getMetadata( METADATA_KEY.tab, target.constructor - ) || new Map(); - return tabcompleterMetadata; + ) || new Map() + return tabcompleterMetadata } function getPluginListenerMetadata(target: any) { let listnerMetadata: interfaces.ListenerMetadata[] = Reflect.getMetadata( METADATA_KEY.listener, target.constructor - ) || []; - return listnerMetadata; + ) || [] + return listnerMetadata } function getPluginConfigMetadata(target: any) { let configMetadata: Map = Reflect.getMetadata( METADATA_KEY.config, target.constructor - ) || new Map(); - return configMetadata; + ) || new Map() + return configMetadata } function getPluginStageMetadata(target: any, stage: string) { let stageMetadata: interfaces.ExecMetadata[] = Reflect.getMetadata( METADATA_KEY.stage[stage], target.constructor - ) || []; - return stageMetadata; + ) || [] + return stageMetadata } export {