mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-06 22:19:25 +08:00
* refactor: plugin manager page * fix: bug * feat: addByNpm api * fix: improve the addByNpm * feat: improve applicationPlugins:list api * fix: re-download npm package when restart app * fix: plugin delete api * feat: plugin detail api * feat: zipUrl add api * fix: upload api bug * fix: plugin detail info * feat: upgrade api * fix: upload api * feat: handle plugin load error * feat: support authToken * feat: muti lang * fix: build error * fix: self review * Update plugin-manager.ts * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bugs * fix: detail click and remove isOfficial * fix: upgrade no refresh * fix: file size and type check * fix: bug * fix: upgrade error * fix: bug * fix: bug * fix: plugin card layout * fix: handling exceptional cases * fix: tgz file support * fix: macos compress file * fix: bug * fix: bug * fix: bug * fix: bug * fix: add upgrade npm type * fix: bugs * fix: bug * fix: change plugins static expose url * fix: api prefix * fix: bug * fix: add nginx `/static/plugin/` path * fix: bugs and pr docker build no dts * fix: bug * fix: build tools bug * fix: improve code * fix: build bug * feat: improve plugin info * fix: ui bug * fix: plugin document bug * feat: improve code * feat: improve code * feat: process dev deps check * feat: improve code * feat: process.env.IS_DEV_CMD * fix: do not delete the plugin package * feat: plugin symlink * fix: tsx watch --ignore=./storage/plugins/** * fix: test error * fix: improve code * fix: improve code * fix: emitStartedEvent * fix: improve code * fix: type error * fix: test error * test: console.log * fix: createStoragePluginSymLink * fix: clientStaticMiddleware rename to clientStaticUtils * feat: build tools support plugins folder * fix: 350px * fix: error * feat: client dev support plugin folder * fix: clear cli options * fix: typeError: Converting circular structure to JSON * fix: plugin name * chore: restart application after command * feat: upgrade error & docs * Update v14-changelog.md * Update v14-changelog.md * Update v14-changelog.md * fix: gateway test * refactor(plugin-workflow): add ready state for gracefully tearing down * Revert "chore: restart application after command" This reverts commit 5015274f8e4e06e506e15754b672330330e8c7f8. * chore: stop application whe restart * T 1218 change plugin folder (#2629) * feat: change folder name * feat: change `pm create` command * feat: revert plugin name change * fix: delete samples * feat: change plugins folder * fix: pm create * feat: update docs * fix: link package error * fix: docs * fix: create command * fix: pm add error * fix: create add build * fix: pm creatre + add * feat: add tar command * fix: docs * fix: bug * fix: docs --------- Co-authored-by: chenos <chenlinxh@gmail.com> * feat: docs * Update your-fisrt-plugin.md * Update your-fisrt-plugin.md * chore: application reload * chore: test * fix: pm add error * chore: preset install skip exists plugin * fix: createIfNotExists --------- Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: chareice <chareice@live.com> Co-authored-by: Zhou <zhou.working@gmail.com> Co-authored-by: mytharcher <mytharcher@gmail.com>
335 lines
8.4 KiB
TypeScript
335 lines
8.4 KiB
TypeScript
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
|
import { Mutex } from 'async-mutex';
|
|
import { EventEmitter } from 'events';
|
|
import Application, { ApplicationOptions, MaintainingCommandStatus } from './application';
|
|
import { getErrorLevel } from './errors/handler';
|
|
|
|
type BootOptions = {
|
|
appName: string;
|
|
options: any;
|
|
appSupervisor: AppSupervisor;
|
|
};
|
|
|
|
type AppBootstrapper = (bootOptions: BootOptions) => Promise<void>;
|
|
|
|
type AppStatus = 'initializing' | 'initialized' | 'running' | 'commanding' | 'stopped' | 'error' | 'not_found';
|
|
|
|
export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
|
private static instance: AppSupervisor;
|
|
public runningMode: 'single' | 'multiple' = 'multiple';
|
|
public singleAppName: string | null = null;
|
|
declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
|
public apps: {
|
|
[appName: string]: Application;
|
|
} = {};
|
|
|
|
public appErrors: {
|
|
[appName: string]: Error;
|
|
} = {};
|
|
|
|
public appStatus: {
|
|
[appName: string]: AppStatus;
|
|
} = {};
|
|
|
|
public lastMaintainingMessage: {
|
|
[appName: string]: string;
|
|
} = {};
|
|
|
|
public statusBeforeCommanding: {
|
|
[appName: string]: AppStatus;
|
|
} = {};
|
|
|
|
private appMutexes = {};
|
|
private appBootstrapper: AppBootstrapper = null;
|
|
|
|
private constructor() {
|
|
super();
|
|
|
|
if (process.env.STARTUP_SUBAPP) {
|
|
this.runningMode = 'single';
|
|
this.singleAppName = process.env.STARTUP_SUBAPP;
|
|
}
|
|
}
|
|
|
|
public static getInstance(): AppSupervisor {
|
|
if (!AppSupervisor.instance) {
|
|
AppSupervisor.instance = new AppSupervisor();
|
|
}
|
|
|
|
return AppSupervisor.instance;
|
|
}
|
|
|
|
setAppError(appName: string, error: Error) {
|
|
this.appErrors[appName] = error;
|
|
|
|
this.emit('appError', {
|
|
appName: appName,
|
|
error,
|
|
});
|
|
}
|
|
|
|
hasAppError(appName: string) {
|
|
return !!this.appErrors[appName];
|
|
}
|
|
|
|
clearAppError(appName: string) {
|
|
delete this.appErrors[appName];
|
|
}
|
|
|
|
async reset() {
|
|
const appNames = Object.keys(this.apps);
|
|
for (const appName of appNames) {
|
|
await this.removeApp(appName);
|
|
}
|
|
|
|
this.appBootstrapper = null;
|
|
this.removeAllListeners();
|
|
}
|
|
|
|
async destroy() {
|
|
await this.reset();
|
|
AppSupervisor.instance = null;
|
|
}
|
|
|
|
setAppStatus(appName: string, status: AppStatus, options = {}) {
|
|
if (this.appStatus[appName] === status) {
|
|
return;
|
|
}
|
|
|
|
this.appStatus[appName] = status;
|
|
|
|
this.emit('appStatusChanged', {
|
|
appName,
|
|
status,
|
|
options,
|
|
});
|
|
}
|
|
|
|
getMutexOfApp(appName: string) {
|
|
if (!this.appMutexes[appName]) {
|
|
this.appMutexes[appName] = new Mutex();
|
|
}
|
|
|
|
return this.appMutexes[appName];
|
|
}
|
|
|
|
async bootStrapApp(appName: string, options = {}) {
|
|
await this.getMutexOfApp(appName).runExclusive(async () => {
|
|
if (!this.hasApp(appName)) {
|
|
this.setAppStatus(appName, 'initializing');
|
|
|
|
if (this.appBootstrapper) {
|
|
await this.appBootstrapper({
|
|
appSupervisor: this,
|
|
appName,
|
|
options,
|
|
});
|
|
}
|
|
|
|
if (!this.hasApp(appName)) {
|
|
this.setAppStatus(appName, 'not_found');
|
|
} else if (!this.getAppStatus(appName) || this.getAppStatus(appName) == 'initializing') {
|
|
this.setAppStatus(appName, 'initialized');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async getApp(
|
|
appName: string,
|
|
options: {
|
|
withOutBootStrap?: boolean;
|
|
[key: string]: any;
|
|
} = {},
|
|
) {
|
|
if (!options.withOutBootStrap) {
|
|
await this.bootStrapApp(appName, options);
|
|
}
|
|
|
|
return this.apps[appName];
|
|
}
|
|
|
|
setAppBootstrapper(appBootstrapper: AppBootstrapper) {
|
|
this.appBootstrapper = appBootstrapper;
|
|
}
|
|
|
|
getAppStatus(appName: string, defaultStatus?: AppStatus): AppStatus | null {
|
|
const status = this.appStatus[appName];
|
|
|
|
if (status === undefined && defaultStatus !== undefined) {
|
|
return defaultStatus;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
bootMainApp(options: ApplicationOptions) {
|
|
return new Application(options);
|
|
}
|
|
|
|
hasApp(appName: string): boolean {
|
|
return !!this.apps[appName];
|
|
}
|
|
|
|
// add app into supervisor
|
|
addApp(app: Application) {
|
|
// if there is already an app with the same name, throw error
|
|
if (this.apps[app.name]) {
|
|
throw new Error(`app ${app.name} already exists`);
|
|
}
|
|
|
|
console.log(`add app ${app.name} into supervisor`);
|
|
|
|
this.bindAppEvents(app);
|
|
|
|
this.apps[app.name] = app;
|
|
|
|
this.emit('afterAppAdded', app);
|
|
|
|
if (!this.getAppStatus(app.name) || this.getAppStatus(app.name) == 'not_found') {
|
|
this.setAppStatus(app.name, 'initialized');
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
// get registered app names
|
|
async getAppsNames() {
|
|
const apps = Object.values(this.apps);
|
|
|
|
return apps.map((app) => app.name);
|
|
}
|
|
|
|
async removeApp(appName: string) {
|
|
if (!this.apps[appName]) {
|
|
console.log(`app ${appName} not exists`);
|
|
return;
|
|
}
|
|
|
|
// call app.destroy
|
|
await this.apps[appName].runCommand('destroy');
|
|
}
|
|
|
|
subApps() {
|
|
return Object.values(this.apps).filter((app) => app.name !== 'main');
|
|
}
|
|
|
|
on(eventName: string | symbol, listener: (...args: any[]) => void): this {
|
|
const listeners = this.listeners(eventName);
|
|
const listenerName = listener.name;
|
|
|
|
if (listenerName !== '') {
|
|
const exists = listeners.find((l) => l.name === listenerName);
|
|
|
|
if (exists) {
|
|
super.removeListener(eventName, exists as any);
|
|
}
|
|
}
|
|
|
|
return super.on(eventName, listener);
|
|
}
|
|
|
|
private bindAppEvents(app: Application) {
|
|
app.on('afterDestroy', () => {
|
|
delete this.apps[app.name];
|
|
delete this.appStatus[app.name];
|
|
delete this.appErrors[app.name];
|
|
delete this.lastMaintainingMessage[app.name];
|
|
delete this.statusBeforeCommanding[app.name];
|
|
});
|
|
|
|
app.on('maintainingMessageChanged', ({ message, maintainingStatus }) => {
|
|
if (this.lastMaintainingMessage[app.name] === message) {
|
|
return;
|
|
}
|
|
|
|
this.lastMaintainingMessage[app.name] = message;
|
|
|
|
const appStatus = this.getAppStatus(app.name);
|
|
|
|
if (!maintainingStatus && appStatus !== 'running') {
|
|
return;
|
|
}
|
|
|
|
this.emit('appMaintainingMessageChanged', {
|
|
appName: app.name,
|
|
message,
|
|
status: appStatus,
|
|
command: appStatus == 'running' ? null : maintainingStatus.command,
|
|
});
|
|
});
|
|
|
|
app.on('__started', async (_app, options) => {
|
|
const { maintainingStatus } = options;
|
|
if (
|
|
maintainingStatus &&
|
|
['install', 'upgrade', 'pm.add', 'pm.update', 'pm.enable', 'pm.disable', 'pm.remove'].includes(
|
|
maintainingStatus.command.name,
|
|
)
|
|
) {
|
|
this.setAppStatus(app.name, 'running', {
|
|
refresh: true,
|
|
});
|
|
} else {
|
|
this.setAppStatus(app.name, 'running');
|
|
}
|
|
});
|
|
|
|
app.on('afterStop', async () => {
|
|
this.setAppStatus(app.name, 'stopped');
|
|
});
|
|
|
|
app.on('maintaining', (maintainingStatus: MaintainingCommandStatus) => {
|
|
const { status, command } = maintainingStatus;
|
|
|
|
switch (status) {
|
|
case 'command_begin':
|
|
{
|
|
this.statusBeforeCommanding[app.name] = this.getAppStatus(app.name);
|
|
this.setAppStatus(app.name, 'commanding');
|
|
}
|
|
break;
|
|
|
|
case 'command_running':
|
|
// emit status changed
|
|
// this.emit('appMaintainingStatusChanged', maintainingStatus);
|
|
break;
|
|
case 'command_end':
|
|
{
|
|
const appStatus = this.getAppStatus(app.name);
|
|
// emit status changed
|
|
this.emit('appMaintainingStatusChanged', maintainingStatus);
|
|
|
|
// not change
|
|
if (appStatus == 'commanding') {
|
|
this.setAppStatus(app.name, this.statusBeforeCommanding[app.name]);
|
|
}
|
|
}
|
|
break;
|
|
case 'command_error':
|
|
{
|
|
const errorLevel = getErrorLevel(maintainingStatus.error);
|
|
|
|
if (errorLevel === 'fatal') {
|
|
this.setAppError(app.name, maintainingStatus.error);
|
|
this.setAppStatus(app.name, 'error');
|
|
break;
|
|
}
|
|
|
|
if (errorLevel === 'warn') {
|
|
this.emit('appError', {
|
|
appName: app.name,
|
|
error: maintainingStatus.error,
|
|
});
|
|
}
|
|
|
|
this.setAppStatus(app.name, this.statusBeforeCommanding[app.name]);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
applyMixins(AppSupervisor, [AsyncEmitter]);
|