feat(gateway): response cli result when run nocobase command (#2563)
* chore(gateway): refresh message in websocket * chore(gateway): throw error when cli error * chore(gateway): await ipc server response * chore: notification message * fix: build * chore: notification type * feat: notification --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
1694eb6d73
commit
797f566d70
@ -51,6 +51,7 @@ export class Application {
|
||||
public pm: PluginManager;
|
||||
public devDynamicImport: DevDynamicImport;
|
||||
public requirejs: RequireJS;
|
||||
public notification;
|
||||
loading = true;
|
||||
maintained = false;
|
||||
maintaining = false;
|
||||
@ -128,14 +129,18 @@ export class Application {
|
||||
this.ws.on('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(data.payload);
|
||||
if (data?.payload?.refresh) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (data.type === 'notification') {
|
||||
this.notification[data.payload?.type || 'info']({ message: data.payload?.message });
|
||||
return;
|
||||
}
|
||||
const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING';
|
||||
if (maintaining) {
|
||||
this.maintaining = true;
|
||||
this.error = data.payload;
|
||||
} else if (this.maintaining) {
|
||||
// && !this.maintained
|
||||
window.location.reload();
|
||||
return;
|
||||
} else {
|
||||
this.maintaining = false;
|
||||
this.maintained = true;
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { App } from 'antd';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { useApp } from '../application';
|
||||
|
||||
const AppInner = memo(({ children }: { children: React.ReactNode }) => {
|
||||
const app = useApp();
|
||||
const { notification } = App.useApp();
|
||||
const apiClient = useAPIClient();
|
||||
|
||||
useEffect(() => {
|
||||
apiClient.notification = notification;
|
||||
app.notification = notification;
|
||||
}, [notification]);
|
||||
|
||||
return <>{children}</>;
|
||||
|
@ -260,22 +260,11 @@ export const PluginCard = (props: { data: IPluginData }) => {
|
||||
size={'small'}
|
||||
onChange={async (checked, e) => {
|
||||
e.stopPropagation();
|
||||
// modal.warning({
|
||||
// title: checked ? t('Plugin starting') : t('Plugin stopping'),
|
||||
// content: t('The application is reloading, please do not close the page.'),
|
||||
// okButtonProps: {
|
||||
// style: {
|
||||
// display: 'none',
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
await api.request({
|
||||
url: `pm:${checked ? 'enable' : 'disable'}/${name}`,
|
||||
});
|
||||
// window.location.reload();
|
||||
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
||||
}}
|
||||
defaultChecked={enabled}
|
||||
checked={enabled}
|
||||
></Switch>,
|
||||
].filter(Boolean),
|
||||
[api, enabled, navigate, id, name, t],
|
||||
|
@ -253,6 +253,31 @@ describe('gateway', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive refresh true when app installed', async () => {
|
||||
await connectClient(port);
|
||||
const app = new Application({
|
||||
database: {
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
},
|
||||
});
|
||||
|
||||
await waitSecond();
|
||||
|
||||
await app.runCommand('start');
|
||||
await app.runCommand('install');
|
||||
|
||||
await waitSecond();
|
||||
|
||||
expect(getLastMessage()).toMatchObject({
|
||||
type: 'maintaining',
|
||||
payload: {
|
||||
code: 'APP_RUNNING',
|
||||
refresh: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive app running message when command end', async () => {
|
||||
await connectClient(port);
|
||||
const app = new Application({
|
||||
|
@ -2,6 +2,7 @@ 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;
|
||||
@ -90,7 +91,7 @@ export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
||||
AppSupervisor.instance = null;
|
||||
}
|
||||
|
||||
setAppStatus(appName: string, status: AppStatus) {
|
||||
setAppStatus(appName: string, status: AppStatus, options = {}) {
|
||||
if (this.appStatus[appName] === status) {
|
||||
return;
|
||||
}
|
||||
@ -100,6 +101,7 @@ export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
||||
this.emit('appStatusChanged', {
|
||||
appName,
|
||||
status,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
@ -257,8 +259,18 @@ export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
||||
});
|
||||
});
|
||||
|
||||
app.on('__started', async () => {
|
||||
app.on('__started', async (_app, options) => {
|
||||
const { maintainingStatus } = options;
|
||||
if (
|
||||
maintainingStatus &&
|
||||
['install', 'upgrade', 'pm.enable', 'pm.disable'].includes(maintainingStatus.command.name)
|
||||
) {
|
||||
this.setAppStatus(app.name, 'running', {
|
||||
refresh: true,
|
||||
});
|
||||
} else {
|
||||
this.setAppStatus(app.name, 'running');
|
||||
}
|
||||
});
|
||||
|
||||
app.on('afterStop', async () => {
|
||||
@ -266,7 +278,7 @@ export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
||||
});
|
||||
|
||||
app.on('maintaining', (maintainingStatus: MaintainingCommandStatus) => {
|
||||
const { status } = maintainingStatus;
|
||||
const { status, command } = maintainingStatus;
|
||||
|
||||
switch (status) {
|
||||
case 'command_begin':
|
||||
@ -294,8 +306,22 @@ export class AppSupervisor extends EventEmitter implements AsyncEmitter {
|
||||
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;
|
||||
}
|
||||
|
@ -360,7 +360,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
}
|
||||
|
||||
createCli() {
|
||||
return new Command('nocobase')
|
||||
const command = new Command('nocobase')
|
||||
.usage('[command] [options]')
|
||||
.hook('preAction', async (_, actionCommand) => {
|
||||
this.activatedCommand = {
|
||||
@ -385,9 +385,15 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
await this.restart();
|
||||
}
|
||||
});
|
||||
|
||||
command.exitOverride((err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
async runAsCLI(argv = process.argv, options?: ParseOptions) {
|
||||
async runAsCLI(argv = process.argv, options?: ParseOptions & { throwError?: boolean }) {
|
||||
if (this.activatedCommand) {
|
||||
return;
|
||||
}
|
||||
@ -404,12 +410,21 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
return command;
|
||||
} catch (error) {
|
||||
console.log(`run command ${this.activatedCommand.name} error:`, error);
|
||||
if (!this.activatedCommand) {
|
||||
this.activatedCommand = {
|
||||
name: 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
this.setMaintaining({
|
||||
status: 'command_error',
|
||||
command: this.activatedCommand,
|
||||
error,
|
||||
});
|
||||
|
||||
if (options?.throwError) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
this.activatedCommand = null;
|
||||
}
|
||||
@ -439,7 +454,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
this.setMaintainingMessage('emit afterStart');
|
||||
await this.emitAsync('afterStart', this, options);
|
||||
await this.emitAsync('__started', this, options);
|
||||
await this.emitAsync('__started', this, {
|
||||
maintainingStatus: lodash.cloneDeep(this._maintainingCommandStatus),
|
||||
});
|
||||
|
||||
this.stopped = false;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Application from '../application';
|
||||
import { PluginCommandError } from '../errors/plugin-command-error';
|
||||
|
||||
export default (app: Application) => {
|
||||
const pm = app.command('pm');
|
||||
@ -21,11 +22,7 @@ export default (app: Application) => {
|
||||
try {
|
||||
await app.pm.enable(plugins);
|
||||
} catch (error) {
|
||||
app.log.debug(`Failed to enable plugin: ${error.message}`);
|
||||
app.setMaintainingMessage(`Failed to enable plugin: ${error.message}`);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), 10000);
|
||||
});
|
||||
throw new PluginCommandError(`Failed to enable plugin: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -35,11 +32,7 @@ export default (app: Application) => {
|
||||
try {
|
||||
await app.pm.disable(plugins);
|
||||
} catch (error) {
|
||||
app.log.debug(`Failed to disable plugin: ${error.message}`);
|
||||
app.setMaintainingMessage(`Failed to disable plugin: ${error.message}`);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), 10000);
|
||||
});
|
||||
throw new PluginCommandError(`Failed to disable plugin: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
16
packages/core/server/src/errors/handler.ts
Normal file
16
packages/core/server/src/errors/handler.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { PluginCommandError } from './plugin-command-error';
|
||||
|
||||
type ErrorLevel = 'fatal' | 'silly' | 'warn';
|
||||
|
||||
export function getErrorLevel(e: Error): ErrorLevel {
|
||||
// @ts-ignore
|
||||
if (e.code === 'commander.unknownCommand') {
|
||||
return 'silly';
|
||||
}
|
||||
|
||||
if (e instanceof PluginCommandError) {
|
||||
return 'warn';
|
||||
}
|
||||
|
||||
return 'fatal';
|
||||
}
|
1
packages/core/server/src/errors/plugin-command-error.ts
Normal file
1
packages/core/server/src/errors/plugin-command-error.ts
Normal file
@ -0,0 +1 @@
|
||||
export class PluginCommandError extends Error {}
|
@ -207,7 +207,8 @@ export class Gateway extends EventEmitter {
|
||||
const ipcClient = await this.tryConnectToIPCServer();
|
||||
|
||||
if (ipcClient) {
|
||||
ipcClient.write({ type: 'passCliArgv', payload: { argv: process.argv } });
|
||||
await ipcClient.write({ type: 'passCliArgv', payload: { argv: process.argv } });
|
||||
// should wait for server response
|
||||
ipcClient.close();
|
||||
return;
|
||||
}
|
||||
|
@ -1,21 +1,38 @@
|
||||
import net from 'net';
|
||||
import * as events from 'events';
|
||||
|
||||
const writeJSON = (socket: net.Socket, data: object) => {
|
||||
export const writeJSON = (socket: net.Socket, data: object) => {
|
||||
socket.write(JSON.stringify(data) + '\n', 'utf8');
|
||||
};
|
||||
|
||||
export class IPCSocketClient {
|
||||
export class IPCSocketClient extends events.EventEmitter {
|
||||
client: net.Socket;
|
||||
|
||||
constructor(client: net.Socket) {
|
||||
super();
|
||||
|
||||
this.client = client;
|
||||
|
||||
this.client.on('data', (data) => {
|
||||
const dataAsString = data.toString();
|
||||
const messages = dataAsString.split('\n');
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dataObj = JSON.parse(message);
|
||||
|
||||
this.handleServerMessage(dataObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async getConnection(serverPath: string) {
|
||||
return new Promise<IPCSocketClient>((resolve, reject) => {
|
||||
const client = net.createConnection({ path: serverPath }, () => {
|
||||
// 'connect' listener.
|
||||
console.log('connected to server!');
|
||||
resolve(new IPCSocketClient(client));
|
||||
});
|
||||
client.on('error', (err) => {
|
||||
@ -24,11 +41,29 @@ export class IPCSocketClient {
|
||||
});
|
||||
}
|
||||
|
||||
async handleServerMessage({ type, payload }) {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
console.error(payload.message);
|
||||
break;
|
||||
case 'success':
|
||||
console.log('success');
|
||||
break;
|
||||
default:
|
||||
console.log({ type, payload });
|
||||
break;
|
||||
}
|
||||
|
||||
this.emit('response', { type, payload });
|
||||
}
|
||||
|
||||
close() {
|
||||
this.client.end();
|
||||
}
|
||||
|
||||
write(data: any) {
|
||||
writeJSON(this.client, data);
|
||||
|
||||
return new Promise((resolve) => this.once('response', resolve));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import net from 'net';
|
||||
import fs from 'fs';
|
||||
import { Gateway } from '../gateway';
|
||||
import { AppSupervisor } from '../app-supervisor';
|
||||
import { writeJSON } from './ipc-socket-client';
|
||||
|
||||
export class IPCSocketServer {
|
||||
socketServer: net.Server;
|
||||
@ -34,7 +34,20 @@ export class IPCSocketServer {
|
||||
|
||||
const dataObj = JSON.parse(message);
|
||||
|
||||
IPCSocketServer.handleClientMessage(dataObj);
|
||||
IPCSocketServer.handleClientMessage(dataObj)
|
||||
.then(() => {
|
||||
writeJSON(c, {
|
||||
type: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
writeJSON(c, {
|
||||
type: 'error',
|
||||
payload: {
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -53,8 +66,14 @@ export class IPCSocketServer {
|
||||
const argv = payload.argv;
|
||||
|
||||
const mainApp = await AppSupervisor.getInstance().getApp('main');
|
||||
mainApp.runAsCLI(argv);
|
||||
|
||||
return mainApp.runAsCLI(argv, {
|
||||
from: 'node',
|
||||
throwError: true,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unknown message type ${type}`);
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -59,6 +59,16 @@ export class WSServer {
|
||||
});
|
||||
});
|
||||
|
||||
AppSupervisor.getInstance().on('appError', async ({ appName, error }) => {
|
||||
this.sendToConnectionsByTag('app', appName, {
|
||||
type: 'notification',
|
||||
payload: {
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
AppSupervisor.getInstance().on('appMaintainingMessageChanged', async ({ appName, message, command, status }) => {
|
||||
const app = await AppSupervisor.getInstance().getApp(appName, {
|
||||
withOutBootStrap: true,
|
||||
@ -76,16 +86,18 @@ export class WSServer {
|
||||
});
|
||||
});
|
||||
|
||||
AppSupervisor.getInstance().on('appStatusChanged', async ({ appName, status }) => {
|
||||
AppSupervisor.getInstance().on('appStatusChanged', async ({ appName, status, options }) => {
|
||||
const app = await AppSupervisor.getInstance().getApp(appName, {
|
||||
withOutBootStrap: true,
|
||||
});
|
||||
|
||||
const payload = getPayloadByErrorCode(status, { app, appName });
|
||||
console.log(`send payload ${JSON.stringify(payload)}`);
|
||||
this.sendToConnectionsByTag('app', appName, {
|
||||
type: 'maintaining',
|
||||
payload,
|
||||
payload: {
|
||||
...payload,
|
||||
...options,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user