diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.http b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.http index 9e5ed9c6..4c3e9235 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.http +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.http @@ -8,7 +8,7 @@ Authorization: {{token}} "timeout": 10, "command": "echo 这是日志@{{ hostAddress }}\nsleep 1\necho @{{ hostName }}", "parameterSchema": "[]", - "hostIdList": [1,7] + "hostIdList": [1] } diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.java index a72f8d41..4cda0cfc 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/controller/ExecController.java @@ -77,7 +77,7 @@ public class ExecController { } @PostMapping("/tail-log") - @Operation(summary = "查看批量执行日志") + @Operation(summary = "查看执行日志") @PreAuthorize("@ss.hasAnyPermission('asset:exec:exec-command', 'asset:exec-log:query')") public String getExecLogTailToken(@Validated @RequestBody ExecLogTailRequest request) { return execService.getExecLogTailToken(request); @@ -85,9 +85,9 @@ public class ExecController { @OperatorLog(ExecOperatorType.DOWNLOAD_HOST_LOG) @GetMapping("/download-log") - @Operation(summary = "下载执行日志文件") + @Operation(summary = "下载执行日志") @PreAuthorize("@ss.hasAnyPermission('asset:exec:exec-command', 'asset:exec-log:query')") - public void downloadLogFile(@RequestParam("id") Long id, HttpServletResponse response) { + public void downloadExecLogFile(@RequestParam("id") Long id, HttpServletResponse response) { execService.downloadLogFile(id, response); } diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/entity/vo/ExecCommandVO.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/entity/vo/ExecCommandVO.java index db0c1035..cda023d4 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/entity/vo/ExecCommandVO.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/entity/vo/ExecCommandVO.java @@ -26,6 +26,9 @@ public class ExecCommandVO implements Serializable { @Schema(description = "id") private Long id; + @Schema(description = "执行状态") + private String status; + @Schema(description = "主机 id 映射") private List hosts; diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandHandler.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandHandler.java index 80a67e43..0d0ef508 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandHandler.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandHandler.java @@ -152,6 +152,8 @@ public class ExecCommandHandler implements IExecCommandHandler { log.info("ExecCommandHandler.updateStatus finish id: {}, effect: {}", id, effect); } + // TODO timeout + @Override public void write(String msg) { this.executor.write(msg); diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/service/impl/ExecServiceImpl.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/service/impl/ExecServiceImpl.java index 99c936d3..1f2efbac 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/service/impl/ExecServiceImpl.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/service/impl/ExecServiceImpl.java @@ -167,6 +167,7 @@ public class ExecServiceImpl implements ExecService { .collect(Collectors.toList()); return ExecCommandVO.builder() .id(execId) + .status(execLog.getStatus()) .hosts(hostResult) .build(); } diff --git a/orion-ops-ui/src/api/exec/exec.ts b/orion-ops-ui/src/api/exec/exec.ts index 552e0c2d..5dc20f11 100644 --- a/orion-ops-ui/src/api/exec/exec.ts +++ b/orion-ops-ui/src/api/exec/exec.ts @@ -9,7 +9,7 @@ export interface ExecCommandRequest { timeout?: number; command?: string; parameterSchema?: string; - hostIdList?: number[]; + hostIdList?: Array; } /** @@ -20,11 +20,22 @@ export interface ExecInterruptRequest { hostLogId?: number; } +/** + * 中断命令请求 + */ +export interface ExecTailRequest { + execId?: number; + hostExecIdList?: Array; +} + /** * 执行命令响应 */ export interface ExecCommandResponse { id: number; + status: string; + startTime: number; + finishTime: number; hosts: Array; } @@ -70,3 +81,17 @@ export function interruptExec(request: ExecInterruptRequest) { export function interruptHostExec(request: ExecInterruptRequest) { return axios.put('/asset/exec/interrupt-host', request); } + +/** + * 查看执行日志 + */ +export function getExecLogTailToken(request: ExecTailRequest) { + return axios.post('/asset/exec/tail-log', request); +} + +/** + * 下载执行日志文件 + */ +export function downloadExecLogFile(id: number) { + return axios.get('/asset/exec/download-log', { unwrap: true, params: { id } }); +} diff --git a/orion-ops-ui/src/components/view/log-appender/appender.const.ts b/orion-ops-ui/src/components/view/log-appender/appender.const.ts new file mode 100644 index 00000000..0d74d81c --- /dev/null +++ b/orion-ops-ui/src/components/view/log-appender/appender.const.ts @@ -0,0 +1,61 @@ +import type { IDisposable, ITerminalOptions } from 'xterm'; +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { SearchAddon } from 'xterm-addon-search'; +import { CanvasAddon } from 'xterm-addon-canvas'; + +// appender 配置 +export const AppenderOption: ITerminalOptions = { + theme: { + foreground: '#FFFFFF', + background: '#212529', + selectionBackground: '#B5D5FF', + }, + rightClickSelectsWord: true, + disableStdin: true, + cursorStyle: 'bar', + cursorBlink: false, + fastScrollModifier: 'alt', + fontSize: 14, + lineHeight: 1.08, + convertEol: true, +}; + +// dom 引用 +export interface LogDomRef { + id: number; + el: HTMLElement; +} + +// appender 配置 +export interface LogAppenderConf { + id: number; + el: HTMLElement; + terminal: Terminal; + addons: LogAddons; +} + +// appender 插件 +export interface LogAddons extends Record { + fit: FitAddon; + canvas: CanvasAddon; + search: SearchAddon; +} + +// 执行日志 appender 定义 +export interface ILogAppender { + // 初始化 + init(refs: Array): Promise; + + // 自适应 + fit(): void; + + // 关闭 client + closeClient(): void; + + // 关闭 view + closeView(): void; + + // 关闭 + close(): void; +} diff --git a/orion-ops-ui/src/components/view/log-appender/log-appender.ts b/orion-ops-ui/src/components/view/log-appender/log-appender.ts new file mode 100644 index 00000000..5dee1ac1 --- /dev/null +++ b/orion-ops-ui/src/components/view/log-appender/log-appender.ts @@ -0,0 +1,148 @@ +import type { ExecTailRequest } from '@/api/exec/exec'; +import { getExecLogTailToken } from '@/api/exec/exec'; +import type { ILogAppender, LogAddons, LogAppenderConf, LogDomRef } from './appender.const'; +import { AppenderOption } from './appender.const'; +import { Terminal } from 'xterm'; +import { webSocketBaseUrl } from '@/utils/env'; +import { Message } from '@arco-design/web-vue'; +import { createWebSocket } from '@/utils'; +import { FitAddon } from 'xterm-addon-fit'; +import { SearchAddon } from 'xterm-addon-search'; +import { CanvasAddon } from 'xterm-addon-canvas'; + +// todo ping +// todo SEARCH addon +// todo font-size totop copy tobottom selectall clear + +// 执行日志 appender 实现 +export default class LogAppender implements ILogAppender { + + private config: ExecTailRequest; + + private client?: WebSocket; + + private appenderRel: Record; + + private keepAliveTask?: number; + + constructor(config: ExecTailRequest) { + this.config = config; + this.appenderRel = {}; + } + + // 初始化 + async init(logDomRefs: Array) { + // 初始化 appender + this.initAppender(logDomRefs); + // 初始化 client + await this.openClient(); + } + + // 初始化 appender + initAppender(logDomRefs: Array) { + // 打开 log-view + for (let logDomRef of logDomRefs) { + // 初始化 terminal + const terminal = new Terminal(AppenderOption); + // 初始化插件 + const addons = this.initAddons(terminal); + // 打开终端 + terminal.open(logDomRef.el); + // 自适应 + addons.fit.fit(); + this.appenderRel[logDomRef.id] = { + ...logDomRef, + terminal, + addons + }; + } + } + + // 初始化插件 + initAddons(terminal: Terminal): LogAddons { + const fit = new FitAddon(); + const search = new SearchAddon(); + const canvas = new CanvasAddon(); + terminal.loadAddon(fit); + terminal.loadAddon(search); + terminal.loadAddon(canvas); + return { + fit, + search, + canvas + }; + } + + // 初始化 client + async openClient() { + // 获取 token + const { data } = await getExecLogTailToken(this.config); + // 打开会话 + this.client = await createWebSocket(`${webSocketBaseUrl}/exec/log/${data}`); + this.client.onerror = event => { + Message.error('连接失败'); + console.error('log error', event); + }; + this.client.onclose = event => { + console.warn('log close', event); + }; + this.client.onmessage = this.processMessage.bind(this); + // 注册持久化 + this.keepAliveTask = setInterval(() => { + if (this.client?.readyState === WebSocket.OPEN) { + this.client?.send('p'); + } + }, 15000); + } + + // 自适应 + fit(): void { + Object.values(this.appenderRel).forEach(s => { + s.addons?.fit?.fit(); + }); + } + + // 关闭 client + closeClient(): void { + // 关闭 ws + if (this.client && this.client.readyState === WebSocket.OPEN) { + this.client.close(); + } + // 清理持久化 + clearInterval(this.keepAliveTask); + } + + // 关闭 view + closeView(): void { + Object.values(this.appenderRel).forEach(s => { + s.terminal?.dispose(); + if (s.addons) { + Object.values(s.addons).forEach(s => s.dispose()); + } + }); + } + + // 关闭 + close(): void { + this.closeClient(); + this.closeView(); + } + + // 处理消息 + processMessage({ data }: MessageEvent) { + // pong + if (data === 'p') { + return; + } + const separatorIndex = data.indexOf('|'); + const id = data.substring(0, separatorIndex); + const text = data.substring(separatorIndex + 1, data.length); + // 获取 appender + const appender = this.appenderRel[id]; + if (!appender) { + return; + } + appender.terminal.write(text); + } + +} diff --git a/orion-ops-ui/src/utils/index.ts b/orion-ops-ui/src/utils/index.ts index 314c1244..53b60536 100644 --- a/orion-ops-ui/src/utils/index.ts +++ b/orion-ops-ui/src/utils/index.ts @@ -191,12 +191,29 @@ export const resetObject = (obj: any, ignore: string[] = []) => { export const objectTruthKeyCount = (obj: any, ignore: string[] = []) => { return Object.keys(obj) .filter(s => !ignore.includes(s)) - .reduce(function (acc, curr) { + .reduce(function(acc, curr) { const currVal = obj[curr]; return acc + ~~(currVal !== undefined && currVal !== null && currVal?.length !== 0 && currVal !== ''); }, 0); }; +/** + * 创建 websocket + */ +export const createWebSocket = async (url: string) => { + return new Promise((resolve, reject) => { + const socket = new WebSocket(url); + + socket.addEventListener('open', () => { + resolve(socket); + }); + + socket.addEventListener('error', (error) => { + reject(error); + }); + }); +}; + /** * 休眠 */ @@ -224,7 +241,7 @@ export function detectZoom() { * 获取唯一的 UUID */ export function getUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); diff --git a/orion-ops-ui/src/views/exec/exec-command/components/log-panel-view.vue b/orion-ops-ui/src/views/exec/exec-command/components/log-panel-view.vue new file mode 100644 index 00000000..dafe64ea --- /dev/null +++ b/orion-ops-ui/src/views/exec/exec-command/components/log-panel-view.vue @@ -0,0 +1,102 @@ + + + + + + + diff --git a/orion-ops-ui/src/views/exec/exec-command/components/log-panel.vue b/orion-ops-ui/src/views/exec/exec-command/components/log-panel.vue index c97d6abd..0c29eb5d 100644 --- a/orion-ops-ui/src/views/exec/exec-command/components/log-panel.vue +++ b/orion-ops-ui/src/views/exec/exec-command/components/log-panel.vue @@ -1,14 +1,16 @@ @@ -20,81 +22,85 @@ diff --git a/orion-ops-ui/src/views/exec/exec-command/index.vue b/orion-ops-ui/src/views/exec/exec-command/index.vue index f5f9abd9..db9bb3a3 100644 --- a/orion-ops-ui/src/views/exec/exec-command/index.vue +++ b/orion-ops-ui/src/views/exec/exec-command/index.vue @@ -18,14 +18,14 @@