Web UI for arthas #2145 (#2292)

This commit is contained in:
xudaotutou
2022-10-10 22:54:30 +08:00
committed by GitHub
parent 116b2495e9
commit bd9e85e55a
74 changed files with 11626 additions and 0 deletions
+1
View File
@@ -56,6 +56,7 @@
<url>https://github.com/alibaba/arthas</url>
<modules>
<module>web-ui</module>
<module>math-game</module>
<module>common</module>
<module>spy</module>
+25
View File
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.husky/*
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar","statelyai.stately-vscode"]
}
+22
View File
@@ -0,0 +1,22 @@
# a Web client based on HTTP API for arthas
## usage
* Through clicking the button in the top right corner, you can quickly get or clear sessionId
* When you are in trouble, refreshing the page is a good way
* Some features must be used with sessionId. but some features must be used without sessionId
* In classloader module, you must selecte a classloader before using classloader to load class or resourse
## develop
* Strongly recommand devloping with vscode
* TS + Vue3 + Tailwindcss + xstate
* You can view the Graphical http requesting process with xstate
* The final bundle will be placed in `../target/static`
### notice
* When use pull_results, you can't use other cmd, such as ```sc class```.
* The consoleMachine.ts will be replaced perRequestMachine.ts + pinia sooner or later
+83
View File
@@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="/src/main.css" rel="stylesheet" />
<title>Arthas Console</title>
</head>
<body style="background: black;">
<nav class="navbar navbar-expand navbar-light bg-light flex-column flex-md-row bd-navbar">
<a href="https://github.com/alibaba/arthas" target="_blank" title="" class="navbar-brand"><img src="logo.png"
alt="Arthas" title="Welcome to Arthas web console" style="height: 25px;" class="img-responsive"></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="https://arthas.aliyun.com/doc" target="_blank">Documentation
<span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://arthas.aliyun.com/doc/arthas-tutorials.html"
target="_blank">Online Tutorials</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/alibaba/arthas" target="_blank">Github</a>
</li>
</ul>
</div>
<form class="form-inline my-2 my-lg-0">
<div class="col">
<div class="input-group ">
<div class="input-group-prepend">
<span class="input-group-text" id="ip-addon">IP</span>
</div>
<input value="127.0.0.1" v-model="ip" type="text" class="form-control" name="ip" id="ip"
placeholder="please enter ip address" aria-label="ip" aria-describedby="ip-addon">
</div>
</div>
<div class="col">
<div class="input-group ">
<div class="input-group-prepend">
<span class="input-group-text" id="port-addon">Port</span>
</div>
<input value="3658" v-model="port" type="text" class="form-control" name="port" id="port"
placeholder="please enter port" aria-label="port" aria-describedby="port-addon">
</div>
</div>
<div class="col-inline">
<button title="connect" type="button" class="btn btn-info form-control"
onclick="startConnect()">Connect</button>
<button title="disconnect" type="button" class="btn btn-info form-control"
onclick="disconnect()">Disconnect</button>
<a target="_blank" href="arthas-output/" class="btn btn-info" role="button">Arthas Output</a>
</div>
</form>
</nav>
<div class="container-fluid px-0">
<div class="col px-0" id="terminal-card">
<div id="terminal"></div>
</div>
</div>
<div title="fullscreen" id="fullSc" class="fullSc">
<button id="fullScBtn" onclick="xtermFullScreen()"><img src="fullsc.png"></button>
</div>
<script src="/src/web-console.js" type="module"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
{
"name": "arthaswebconsole",
"private": false,
"version": "0.1.4",
"scripts": {
"dev": "vite --port 8000",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/vue": "^1.6.6",
"@heroicons/vue": "^1.0.6",
"@highlightjs/vue-plugin": "^2.1.0",
"@xstate/vue": "^2.0.0",
"bootstrap": "4.6.2",
"daisyui": "^2.31.0",
"echarts": "^5.3.3",
"global": "^4.4.0",
"highlight.js": "^11.6.0",
"jquery": "^3.6.1",
"npminstall": "^6.5.1",
"pinia": "^2.0.15",
"popper.js": "^1.16.1",
"typings": "^2.1.1",
"vue": "^3.2.25",
"vue-router": "4",
"xstate": "^4.32.1",
"xterm": "^5.0.0"
},
"devDependencies": {
"@types/node": "^18.7.2",
"@vitejs/plugin-vue": "^2.3.3",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.4",
"typescript": "^4.7.4",
"vite": "^2.9.9",
"vue-tsc": "^0.34.7"
}
}
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+20
View File
@@ -0,0 +1,20 @@
#terminal:-webkit-full-screen{
background-color: rgb(255, 255, 12);
}
.container {
width: 100%;
min-height: 600px;
}
.fullSc {
z-index: 10000;
position: fixed;
top: 25%;
left: 90%;
display: none;
}
#fullScBtn {
border-radius:17px;
border: 0;
cursor: pointer;
background-color: black;
}
+202
View File
@@ -0,0 +1,202 @@
import "bootstrap";
import 'bootstrap/dist/css/bootstrap.min.css';
import $ from "jquery"
import "xterm/css/xterm.css"
import { Terminal } from "xterm"
var ws;
var xterm =new Terminal()
const DEFAULT_SCROLL_BACK = 1000
const MAX_SCROLL_BACK = 9999999
const MIN_SCROLL_BACK = 1
$(function () {
var url = window.location.href;
var ip = getUrlParam('ip');
var port = getUrlParam('port');
if (ip != '' && ip != null) {
$('#ip').val(ip);
} else {
$('#ip').val(window.location.hostname);
}
if (port != '' && port != null) {
$('#port').val(port);
}
if (port == null && location.port == "8563") {
$('#port').val(8563);
}
var iframe = getUrlParam('iframe');
if (iframe != null && iframe != 'false') {
$("nav").hide()
}
startConnect(true);
});
/** get params in url **/
function getUrlParam(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function getCharSize() {
var tempDiv = $('<div />').attr({ 'role': 'listitem' });
var tempSpan = $('<div />').html('qwertyuiopasdfghjklzxcvbnm');
tempDiv.append(tempSpan);
$("html body").append(tempDiv);
var size = {
width: tempSpan.outerWidth() / 26,
height: tempSpan.outerHeight(),
left: tempDiv.outerWidth() - tempSpan.outerWidth(),
top: tempDiv.outerHeight() - tempSpan.outerHeight(),
};
tempDiv.remove();
return size;
}
function getWindowSize() {
var e = window;
var a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
var terminalDiv = document.getElementById("terminal-card");
var terminalDivRect = terminalDiv.getBoundingClientRect();
return {
width: terminalDivRect.width,
height: e[a + 'Height'] - terminalDivRect.top
};
}
function getTerminalSize() {
var charSize = getCharSize();
var windowSize = getWindowSize();
console.log('charsize');
console.log(charSize);
console.log('windowSize');
console.log(windowSize);
return {
cols: Math.floor((windowSize.width - charSize.left) / 10),
rows: Math.floor((windowSize.height - charSize.top) / 17)
};
}
/** init websocket **/
function initWs(ip, port) {
var path = 'ws://' + ip + ':' + port + '/ws';
ws = new WebSocket(path);
}
/** init xterm **/
function initXterm(cols, rows, scrollback) {
let scrollNumber = parseInt(scrollback, 10)
xterm = new Terminal({
cols: cols,
rows: rows,
screenReaderMode: false,
rendererType: 'canvas',
convertEol: true,
scrollback: isValidNumber(scrollNumber) ? scrollNumber : DEFAULT_SCROLL_BACK
});
}
function isValidNumber(scrollNumber) {
return scrollNumber >= MIN_SCROLL_BACK &&
scrollNumber <= MAX_SCROLL_BACK;
}
/** begin connect **/
window.startConnect = function startConnect(silent) {
var ip = $('#ip').val();
var port = $('#port').val();
if (ip == '' || port == '') {
alert('Ip or port can not be empty');
return;
}
if (ws != null) {
alert('Already connected');
return;
}
// init webSocket
initWs(ip, port);
ws.onerror = function () {
ws.close();
ws = null;
!silent && alert('Connect error');
};
ws.onopen = function () {
console.log('open');
$('#fullSc').show();
var terminalSize = getTerminalSize()
let scrollback = getUrlParam('scrollback');
console.log('terminalSize')
console.log(terminalSize)
// init xterm
initXterm(terminalSize.cols, terminalSize.rows, scrollback)
ws.onmessage = function (event) {
if (event.type === 'message') {
var data = event.data;
xterm.write(data);
}
};
xterm.open(document.getElementById('terminal'));
console.log(xterm)
// xterm = new Terminal()
xterm.onData(function (data) {
ws.send(JSON.stringify({ action: 'read', data: data }))
});
ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows }));
window.setInterval(function () {
if (ws != null && ws.readyState === 1) {
ws.send(JSON.stringify({ action: 'read', data: "" }));
}
}, 30000);
}
}
window.disconnect = function disconnect() {
try {
ws.close();
ws.onmessage = null;
ws.onclose = null;
ws = null;
xterm.dispose();
$('#fullSc').hide();
alert('Connection was closed successfully!');
} catch (e) {
alert('No connection, please start connect first.');
}
}
/** full screen show **/
window.xtermFullScreen = function xtermFullScreen() {
var ele = document.getElementById('terminal-card');
requestFullScreen(ele);
}
function requestFullScreen(element) {
var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
if (requestMethod) {
requestMethod.call(element);
} else if (typeof window.ActiveXObject !== "undefined") {
var wscript = new ActiveXObject("WScript.Shell");
if (wscript !== null) {
wscript.SendKeys("{F11}");
}
}
}
window.addEventListener('resize', function () {
if (ws !== undefined && ws !== null) {
let terminalSize = getTerminalSize();
ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows }));
xterm.resize(terminalSize.cols, terminalSize.rows);
}
});
@@ -0,0 +1,24 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./ui/index.html",
"./ui/src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
keyframes: {
},
animation: {
'spin-rev-pause':'0.3s linear 0s infinite reverse both pause spin',
'spin-rev-running':'0.3s linear 0s infinite reverse both running spin'
},
},
daisyui: {
themes: ["corporate"],
},
},
plugins: [
require("daisyui")
],
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*":["ui/src/*"]
},
"experimentalDecorators": true
},
"include": ["ui/src/**/*.ts", "ui/src/**/*.d.ts", "ui/src/**/*.tsx", "ui/src/**/*.vue","ui/tests/**/*.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" data-theme="corporate">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="../public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web UI</title>
</head>
<body>
<div id="app">
</div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
import NavAside from "@/components/routeTo/NavAside.vue";
import NavHeader from "@/components/NavHeader.vue";
import ErrDialog from "@/components/dialog/ErrDialog.vue";
import SuccessDialog from "@/components/dialog/SuccessDialog.vue";
import { onBeforeUnmount } from "vue";
import machine from "./machines/consoleMachine";
import { interpret } from "xstate";
import { fetchStore } from "./stores/fetch";
import InputDialog from "./components/dialog/InputDialog.vue";
import { publicStore } from "./stores/public";
import WarnDialog from "./components/dialog/WarnDialog.vue";
const fetchS = fetchStore()
const publicS = publicStore()
onBeforeUnmount(() => {
const actor = interpret(machine)
actor.start()
console.log('asdfasdf')
actor.send("INIT")
actor.send({
type: "SUBMIT",
value: {
action: "interrupt_job",
} as AsyncReq
})
})
// :class='{"pointer-events-none":fetchS.jobRunning}'
</script>
<template>
<div class=" h-screen flex flex-col">
<nav-header class="h-[10vh]"></nav-header>
<div class=" flex-auto h-[90vh] overflow-auto">
<div class="flex flex-row h-full">
<nav-aside ></nav-aside>
<div class="flex-auto overflow-auto h-[90vh] w-[90vw]">
<router-view>
</router-view>
</div>
</div>
</div>
</div>
<err-dialog v-if="publicS.isErr" />
<success-dialog v-if="publicS.isSuccess" />
<input-dialog v-if="publicS.isInput " />
<warn-dialog v-if="publicS.isWarn"></warn-dialog>
</template>
+722
View File
@@ -0,0 +1,722 @@
type SessionAction =
| "join_session"
| "init_session"
| "close_session";
type ResState = "SCHEDULED" | "SUCCEEDED" | "FAILED" | "REFUSED";
type MergeObj<T extends Record<string, any>, U extends Record<string, any>> =
T extends T ? {
[k in (keyof T | keyof U)]: k extends keyof T ? T[k]
: U[k];
}
: never;
type JobId<T extends Record<string, any>> = T extends T
? MergeObj<T, Record<"jobId", number>>
: never;
type SessionId<T extends Record<string, any>> = T extends T
? MergeObj<T, { sessionId?: string }>
: never;
// type Command<T extends Record<string|"results", any>> = T extends T
// ? T["results"][0] extends JobId<CommandResult>
// ? "command" extends keyof T["results"][0]
// ? MergeObj<T, {command: T["results"][0]["command"]}>
// : never
// : MergeObj<T, { command: never }>
// : never;
type Command<T extends Record<string | "results", any>> = T extends T
? MergeObj<T, { command: string }>
: never;
type CommonAction<T> = T extends T ? MergeObj<
T,
{ action: "exec" }
>
: never;
type unionExclude<T, U> = T extends T ? Exclude<T, U> : T;
// 数值计算
type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = [],
> = Arr["length"] extends Length ? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;
type Sub1<N extends number> = BuildArray<N> extends
[arr1: unknown, ...arr2: infer Rest] ? Rest["length"]
: never;
// 命令T 可以添加N个参数
type StringInclude<T extends string, N extends number, P = string> = T extends T
? N extends 0 ? T
: T | StringInclude<`${T} ${P}`, Sub1<N>>
: never;
type SessionReq =
| {
action: "init_session";
}
| SessionId<{
action: "join_session" | "close_session";
}>;
type AsyncReq = SessionId<
| {
action: "interrupt_job";
}
| MergeObj<
{
action: "async_exec";
},
{
command: "dashboard";
} | {
command: StringInclude<"stack", 3>;
} | {
command: `monitor -c ${number} ${string} ${string}`;
} | {
command: `trace ${string} ${string}`;
} | {
command: `tt -t ${string} ${string}`;
} | {
command: `watch ${string} ${string}`;
}
>
>;
type PullResults = SessionId<{
action: "pull_results";
consumerId?: string;
}>;
type CommandReq = CommonAction<
{
command:
| "sysenv"
| "version"
| "sysprop"
| "pwd"
| "jvm"
| "memory"
| "perfcounter -d"
| "classloader"
| "classloader -a"
| "classloader -t"
| "classloader --url-stat"
| `classloader ${string}`
| `sm -d ${string}`
| `sm ${string}`
| `jad ${string}`
| `dump ${string}`
| "retransform -l"
| `retransform ${string}`
| `retransform --classPattern ${string}`
| `mbean`
| `mbean ${string}`
| `mbean -m ${string}`
| `vmtool --action ${"forceGc" | "getInstances"} ${string}`
| "tt -l"
| `tt -i ${string} -p`
| `tt -s ${string}`
| `profiler ${"list" | "status" | "stop" | "resume" | "getSamples"}`
| `profiler ${string}`
| `stop`
| `ognl ${string}`;
} | {
command: StringInclude<"vmoption" | "thread", 2>;
} | {
command: StringInclude<"sc", 3>;
} | {
command: StringInclude<"heapdump" | "heapdump --live" | "reset", 1>;
}
>;
type ArthasReq = SessionReq | CommandReq | AsyncReq | PullResults;
type ThreadStateCount = {
"NEW": number;
"RUNNABLE": number;
"BLOCKED": number;
"WAITING": number;
"TIMED_WAITING": number;
"TERMINATED": number;
};
type StatusResult = {
type: "status";
statusCode: 0;
} | {
type: "status";
// 实际上不起效果
statusCode: number;
message: string;
};
type InputResult = {
inputStatus: "ALLOW_INPUT" | "DISABLED" | "ALLOW_INTERRUPT";
type: "input_status";
};
type VmOption = MergeObj<
Record<"name" | "origin" | "value", string>,
Record<"writeable", boolean>
>;
type ThreadState = keyof ThreadStateCount;
type ThreadStats = {
cpu: number;
daemon: boolean;
deltaTime: number;
group: "system";
id: number;
interrupted: boolean;
name: string;
priority: number;
state: ThreadState;
time: number;
};
type StackTrace = {
className: string;
fileName: string;
lineNumber: number;
methodName: number;
nativeMethod: boolean;
};
type ThreadInfo = {
blockedCount: number;
blockedTime: number;
inNative: true;
lockOwnerId: number;
lockedMonitors: [];
lockedSynchronizers: [];
stackTrace: StackTrace[];
suspended: boolean;
threadId: number;
threadName: string;
threadState: ThreadState;
waitedCount: number;
waitedTime: number;
};
type BusyThread = {
blockedCount: number;
blockedTime: number;
cpu: number;
daemon: true;
deltaTime: number;
group: string;
id: number;
inNative: boolean;
interrupted: boolean;
lockInfo: {
className: string;
identityHashCode: boolean;
};
lockName: string;
lockOwnerId: number;
lockedMonitors: any[];
lockedSynchronizers: any[];
name: string;
priority: 10;
stackTrace: {
className: string;
fileName: string;
lineNumber: number;
methodName: string;
nativeMethod: boolean;
}[];
state: "WAITING" | "TIMED_WAITING" | "RUNNABLE";
suspended: string;
time: number;
waitedCount: number;
waitedTime: number;
};
type JvmInfo = {
RUNTIME: Record<"name" | "value", string>[];
"CLASS-LOADING": { name: string; value: number | boolean }[];
COMPILATION: { name: string; value: number | string; desc: string }[];
"GARBAGE-COLLECTORS": {
name: string;
value: { name: string; collectionCount: number; collectionTime: number };
desc: string;
}[];
"MEMORY-MANAGERS": { name: string; value: string[] }[];
MEMORY: {
desc: string;
name: string;
value: {
name: string;
init: number;
used: number;
committed: number;
max: number;
} | number;
}[];
"OPERATING-SYSTEM": Record<"name" | "value", string>[];
THREAD: {
name: string;
value: number;
}[];
"FILE-DESCRIPTOR": {
name: string;
value: number;
}[];
};
type MemoryInfo = Record<"heap" | "nonheap" | "buffer_pool", {
max: number;
name: string;
total: number;
type: string;
used: number;
usage?: number;
}[]>;
type RuntimeInfo = {
javaHome: string;
javaVersion: string;
osName: string;
osVersion: string;
processors: number;
systemLoadAverage: number;
timestamp: number;
uptime: number;
};
type ClassDetailInfo = {
annotation: boolean;
annotations: string[];
anonymousClass: boolean;
array: boolean;
classInfo: string;
classLoaderHash: string;
classloader: string[];
codeSource: string;
enum: string;
interface: string;
interfaces: string[];
localClass: string;
memberClass: string;
modifier: string;
name: string;
primitive: boolean;
simpleName: string;
superClass: string[];
synthetic: boolean;
};
type ClassField = {
annotations: string[];
modifier: string;
name: string;
static: boolean;
type: string;
value: any;
};
type MethodInfo = {
classLoaderHash: string;
constructor: boolean;
declaringClass: string;
descriptor: string;
exceptions: string[];
parameters: string[];
annotations: string[];
methodName: string;
modifier: string;
returnType: string;
};
type ClassLoaderNode = {
"hash": string;
"loadedCount": number;
"name": string;
"children": ClassLoaderNode[];
"parent": string;
};
type ClassInfo = MergeObj<ClassDetailInfo, { fields: ClassField[] }>;
type TraceNode = {
children?: TraceNode[];
className: string;
cost: number;
invoking: boolean;
lineNumber: number;
maxCost: number;
methodName: string;
minCost: number;
times: number;
totalCost: number;
type: "method";
} | {
children: never;
exception: string;
lineNumber: number;
message: string;
type: "throw";
} | {
children?: TraceNode[];
classloader: string;
daemon: boolean;
priority: number;
threadId: number;
threadName: string;
timestamp: string;
type: "thread";
};
type TimeFragment = {
"className": string;
"cost": number;
"index": number;
"methodName": string;
"object": string;
"params": {
"expand": number;
"object": number;
}[];
"return": boolean;
"returnObj": string;
"throw": boolean;
"throwExp": string;
"timestamp": string;
};
type MonitorData = {
className: string;
cost: number;
failed: number;
methodName: number;
success: number;
total: number;
};
type CommandResult = {
type: "command";
state: ResState;
command: string;
} | {
type: "version";
version: string;
} | {
type: "sysenv";
env: Record<string, string>;
} | {
type: "sysprop";
props: Record<string, string>;
} | {
type: "vmoption";
vmOptions: vmOption[];
} | {
type: "pwd";
workingDir: string;
} | {
all: boolean;
threadStateCount: ThreadStateCount;
threadStats: ThreadStats[];
threadInfo: never;
busyThreads: never;
type: "thread";
} | {
threadStateCount: never;
threadInfo: ThreadInfo;
threadStats: never;
busyThreads: never;
type: "thread";
} | {
all: boolean;
threadStateCount: never;
busyThreads: BusyThread[];
threadInfo: never;
threadStats: never;
type: "thread";
} | {
jvmInfo: JvmInfo;
type: "jvm";
} | {
memoryInfo: MemoryInfo;
type: "memory";
} | {
perfCounters: {
name: string;
units: string;
value: string | number;
variability: string;
}[];
type: "perfcounter";
} | {
"classLoaderStats": Record<string,Record<"loadedCount"|"numberOfInstance",number>>
urlStats: never;
urls:never;
tree:never;
type: "classloader";
} | {
type: "classloader";
urlStats: {
[x: `{hash":${string},"name:${string}}`]: {
unUsedUrls: string[];
usedUrls: string[];
};
};
"classLoaderStats":never;
urls: never;
tree: never;
} | {
type: "classloader";
urls: never;
"classLoaderStats";
urlStats:never;
classLoaders: ClassLoaderNode[];
tree: boolean;
} | {
type: "classloader";
urls: string[];
urlStats:never;
"classLoaderStats":never;
tree: never;
} | {
classInfo: ClassInfo;
detailed: true;
type: "sc";
segment: 0;
withField: true;
} | {
classNames: string[];
detailed: false;
segment: number;
type: "sc";
withField: false;
} | {
classInfo: ClassDetailInfo;
detailed: true;
jobId: 31365;
segment: 0;
type: "sc";
withField: false;
} | {
detail: true;
methodInfo: MethodInfo;
type: "sm";
} | {
classInfo: {
classLoaderHash: string;
classloader: string[];
name: string;
};
location: string;
mappings: Record<string, number>;
source: string;
type: "jad";
} | {
retransformCount: number;
retransformEntries: {
bytes: string;
className: string;
id: number;
transformCount: number;
}[];
retransformClasses: never;
type: "retransform";
} | {
retransformCount: number;
retransformEntries: never;
retransformClasses: string[];
type: "retransform";
} | {
dumpedClasses: {
classLoaderHash: string;
classloader: string[];
location: string;
name: string;
}[];
type: "dump";
} | {
gcInfos: {
collectionCount: number;
collectionTime: number;
name: string;
}[];
memoryInfo: MemoryInfo;
runtimeInfo: RuntimeInfo;
threads: ThreadStats[];
type: "dashboard";
} | {
mbeanNames: string[];
mbeanMetadata: never;
mbeanAttribute: never;
type: "mbean";
} | {
mbeanNames: never;
mbeanMetadata: {
[x: string]: {
attributes: {
description: string;
is: boolean;
name: string;
readable: boolean;
type: string;
writable: boolean;
openType: Record<string, string>;
}[];
className: string;
constructors: {
description: string;
name: string;
signature: {
description: string;
name: string;
type: string;
}[];
}[];
description: string;
notifications: any[];
operations: {
description: string;
impact: number;
name: string;
returnType: string;
signature: {
description: string;
name: string;
type: string;
}[];
}[];
};
};
mbeanAttribute: never;
type: "mbean";
} | {
mbeanNames: never;
mbeanMetadata: never;
mbeanAttribute: {
[x: string]: {
name: string;
value: string | number | boolean | (number[]);
}[];
};
type: "mbean";
} | {
dumpFile: string;
live: boolean;
type: "heapdump";
} | {
type: "vmtool";
value: string;
} | {
affect: {
classCount: number;
cost: number;
listenerId: number;
methodCount: number;
};
type: "reset";
} | {
classloader: string;
cost: number;
daemon: boolean;
priority: number;
stackTrace: StackTrace[];
threadId: string;
threadName: string;
// date clock
ts: `${string} ${string}`;
type: "stack";
} | {
monitorDataList: MonitorData[];
type: "monitor";
} | {
nodeCount: number;
root: TraceNode;
type: "trace";
} | {
"expand": never;
"replayNo": never;
first: boolean;
timeFragmentList: TimeFragment[];
replayResult: never;
sizeLimit: never;
type: "tt";
} | {
"expand": number;
"replayNo": number;
first: never;
"replayResult": TimeFragment;
timeFragmentList: never;
"sizeLimit": number;
"type": "tt";
} | {
accessPoint: "AtExceptionExit" | "AtEnter" | "AtExit";
className: string;
cost: number;
methodName: string;
sizeLimit: number;
ts: string;
type: "watch";
value: string;
} | {
"action": "list" | "status" | "stop" | "resume" | "getSamples";
"executeResult": string;
"outputFile"?: string;
"type": "profiler";
} | {
type: "ognl";
value: string;
};
type EnchanceResult = {
success: boolean;
effect: Record<"listenerId" | "cost" | "classCount" | "methodCount", number>;
type: "enhancer";
};
type MessageResult = {
message: string;
type: "message";
};
type ArthasResResult = JobId<
| MessageResult
| StatusResult
| InputResult
| CommandResult
| EnchanceResult
>;
type ResBody = Command<
JobId<{
results: ArthasResResult[];
timeExpired: boolean;
jobStatus: "TERMINATED" | "READY";
}>
>;
type CommonRes = {
state: ResState;
sessionId: string;
requestId?: string;
body: ResBody;
};
type AsyncRes = {
state: ResState;
sessionId: string;
requestId?: string;
// results:never;
body: Command<
JobId<{
jobStatus: "READY" | "TERMINATED";
}>
>;
};
type SessionRes = {
sessionId: string;
consumerId: string;
body: never;
state: Exclude<ResState, "FAILED">;
};
type FailRes = SessionId<{
message: string;
state: "FAILED" | "REFUSED";
body: never;
}>;
type ArthasRes = CommonRes | SessionRes | FailRes | AsyncRes;
type BindQS =
| { req: CommandReq; res: CommonRes }
| { req: SessionReq; res: SessionRes }
| { req: AsyncReq; res: AsyncRes };
// autoComplete
type Item = { name: string; value: unknown };
// Tree
interface TreeNode {
children: TreeNode[];
meta: unknown;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,165 @@
<script setup lang="ts">
import { useMachine } from '@xstate/vue';
import { onBeforeMount, Ref, ref, watchEffect } from 'vue';
import { RefreshIcon, LogoutIcon, LoginIcon, MenuIcon, XCircleIcon } from '@heroicons/vue/outline';
import { fetchStore } from '@/stores/fetch';
import machine from "@/machines/consoleMachine"
import { publicStore } from '@/stores/public';
import { interpret } from 'xstate';
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import permachine from '@/machines/perRequestMachine';
const fetchM = useMachine(machine)
const publicS = publicStore()
const { send } = fetchM
const fetchS = fetchStore()
const sessionM = useMachine(machine)
const version = ref("N/A")
const vCmd: CommandReq = {
action: "exec",
command: "version"
}
const restBtnclass: Ref<'animate-spin-rev-pause' | 'animate-spin-rev-running'> = ref('animate-spin-rev-pause')
publicS.getCommonResEffect(fetchM, body => {
const result = body.results[0]
if (result.type === "version") version.value = result.version
})
watchEffect(() => {
if (!fetchS.wait) restBtnclass.value = "animate-spin-rev-pause"
else restBtnclass.value = "animate-spin-rev-running"
})
onBeforeMount(() => {
send("INIT")
send({
type: "SUBMIT",
value: vCmd
})
sessionM.send("INIT")
})
// 手动重来
const reset = () => {
send({
type: "SUBMIT",
value: vCmd
})
}
const interruptEvent = () => {
fetchS.interruptJob()
}
const forceGc = () => {
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: "vmtool --action forceGc "
}).then(
res => publicS.$patch({
isSuccess: true,
SuccessMessage: "GC success!",
})
)
}
const logout = async () => {
restBtnclass.value = "animate-spin-rev-running"
interruptEvent()
sessionM.send("SUBMIT", {
value: {
action: "close_session",
sessionId: undefined
}
})
restBtnclass.value = "animate-spin-rev-pause"
}
const login = async () => {
sessionM.send("SUBMIT", {
value: {
action: "init_session"
}
})
}
const shutdown = () => {
publicS.warnMessage = "Are you sure to stop the arthas? All the Arthas clients connecting to this server will be disconnected."
publicS.warningFn = () => {
fetchS.baseSubmit(interpret(permachine), {
command: "stop",
action: "exec"
})
fetchS.online = false
fetchS.wait = false
}
publicS.isWarn = true
}
const resetAllClass = () => {
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `reset`
}).then(response => {
const result = (response as CommonRes).body.results[0]
if (result.type === "reset") {
// let res = new Map()
// Object.entries(result.affect).forEach(([k, v]) => {
// res.set(k, k === "cost" ? [`${v}ms`] : [v])
// })
// let message = ""
// for(const key in result.affect) {
// m
// }
publicS.isSuccess=true
publicS.SuccessMessage = JSON.stringify(result.affect)
}
})
}
const tools:[string,()=>void][] = [
["forceGc", forceGc],
["shutdown", shutdown],
["reset class", resetAllClass]
]
</script>
<template>
<nav class=" h-[10vh] flex justify-between items-center min-h-max border-b-2 shadow-orange-300">
<a class="w-40 flex items-center justify-center" href="https://arthas.aliyun.com/doc/commands.html" target="_blank">
<img src="@/assets/arthas.png" alt="logo" class=" w-3/4" />
</a>
<div class="flex items-center h-20">
<div class=" mr-4 bg-info text-info-content h-12 rounded-full flex justify-center items-center font-bold p-2">
sessionId: {{fetchS.sessionId}}</div>
<div class=" mr-4 bg-info text-info-content h-12 p-2 rounded-full flex justify-center items-center font-bold">
version:{{ version }}
</div>
<button v-if="fetchS.jobRunning" @click.prevent="interruptEvent"
class="btn-error btn rounded-full h-1/2 p-2 transition mr-4">interrupt</button>
<button class=" rounded-full btn btn-info btn-circle h-12 w-12 flex justify-center items-center mr-4 " @click="reset">
<refresh-icon class="h-3/4 w-3/4" :class="restBtnclass" />
</button>
<button class="hover:opacity-50 h-12 w-12 grid place-items-center rounded-full mr-2 transition-all"
:class="{ 'bg-primary': !fetchS.online, 'bg-error': fetchS.online }">
<LogoutIcon class="h-1/2 w-1/2 text-error-content" @click="logout" v-if="fetchS.online" />
<login-icon class="h-1/2 w-1/2 text-primary-content" @click="login" v-else />
</button>
<Menu as="div" class="relative mr-4">
<MenuButton
class="w-12 h-12 input-btn-style grid place-items-center rounded-full bg-primary transition">
<MenuIcon class="h-3/4 w-3/4 text-primary-content"></MenuIcon>
<!-- <XCirleIcon class="h-3/4 w-3/4"></XCirleIcon> -->
</MenuButton>
<MenuItems class="absolute right-0 top-full input-btn-style mt-4 bg-white px-0 z-10 w-40">
<MenuItem v-slot="{ active }" v-for="(v,i) in tools" :key="i">
<div :class='{ "bg-blue-500 text-primary-content": active }' class="px-4 py-2">
<button @click.prevent="v[1]">{{v[0]}}</button>
</div>
</MenuItem>
</MenuItems>
</Menu>
</div>
</nav>
</template>
<style scoped>
</style>
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public'
import {
Dialog,
DialogPanel,
DialogTitle,
DialogDescription,
TransitionChild,
TransitionRoot
} from '@headlessui/vue'
import { ExclamationCircleIcon } from '@heroicons/vue/outline';
import { onBeforeMount } from 'vue';
const store = publicStore()
function setIsOpen(value: boolean) {
store.isErr = value
}
onBeforeMount(()=>{fetchStore().curPolling.close()})
</script>
<template>
<TransitionRoot :show="true" as="template">
<Dialog @close="setIsOpen" class="min-w-max z-20">
<TransitionChild enter="transition-opacity duration-300" enter-from="opacity-0" enter-to="opacity-100"
leave="transition-opacity duration-300" leave-from="opacity-100" leave-to="opacity-0">
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 grid place-items-center min-w-max">
<TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100" leave="duration-200 ease-in" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95">
<DialogPanel
class=" w-1/3 h-1/2 bg-base-100 p-10 rounded-xl shadow-xl flex flex-col justify-between items-center min-w-max">
<DialogTitle>
<ExclamationCircleIcon class="w-12 h-12 text-error" />
</DialogTitle>
<DialogDescription as="section"
class="flex-auto self-stretch my-10 rounded p-2 break-all max-w-4xl overflow-auto">
reason:
{{ store.ErrMessage }}
</DialogDescription>
<button @click="setIsOpen(false)"
class="btn btn-primary rounded-xl">OK</button>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { publicStore } from '@/stores/public'
import {
Dialog,
DialogPanel,
DialogTitle,
DialogDescription,
TransitionChild,
TransitionRoot
} from '@headlessui/vue'
import { onMounted, ref, onBeforeMount } from 'vue';
const store = publicStore()
const debug = (e: any) => console.log(e)
const inputV = ref("")
onMounted(() => {
inputV.value = store.inputVal
})
function setIsOpen() {
store.inputVal = inputV.value
console.log(store.inputVal, inputV.value)
store.isInput = false
}
function setIsOpenCancel() {
store.isInput = false
}
</script>
<template>
<TransitionRoot as="template" :show="true">
<Dialog @close="setIsOpenCancel" class="min-w-max z-20">
<TransitionChild enter="transition-opacity duration-300" enter-from="opacity-0" enter-to="opacity-100"
leave="transition-opacity duration-300" leave-from="opacity-100" leave-to="opacity-0">
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 grid place-items-center min-w-max">
<TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100" leave="duration-200 ease-in" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95">
<DialogPanel
class=" w-1/3 h-1/2 bg-base-100 p-10 rounded-xl shadow-xl flex flex-col justify-between items-center min-w-max">
<DialogTitle>
input value
</DialogTitle>
<DialogDescription class=" bg-slate-200 my-10 rounded-full w-full flex justify-center px-4">
<input type="text" v-model="inputV" class="bg-slate-200 h-full p-2 w-full focus-visible:outline-none"/>
</DialogDescription>
<div class="flex justify-evenly w-full">
<button @click="setIsOpen"
class="btn btn-primary rounded-xl">OK</button>
<button @click="setIsOpenCancel"
class="btn btn-primary rounded-xl">Cancel</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { publicStore } from '@/stores/public'
import {
Dialog,
DialogPanel,
DialogTitle,
DialogDescription,
TransitionChild,
TransitionRoot
} from '@headlessui/vue'
import { CheckCircleIcon } from '@heroicons/vue/outline';
const store = publicStore()
function setIsOpen(value: boolean) {
store.isSuccess = value
}
</script>
<template>
<TransitionRoot :show="store.isSuccess" as="template">
<Dialog @close="setIsOpen" class="min-w-max z-20">
<TransitionChild enter="transition-opacity duration-300" enter-from="opacity-0" enter-to="opacity-100"
leave="transition-opacity duration-300" leave-from="opacity-100" leave-to="opacity-0">
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 grid place-items-center min-w-max">
<TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100" leave="duration-200 ease-in" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95">
<DialogPanel
class=" w-1/3 h-1/2 bg-base-100 p-10 rounded-xl shadow-xl flex flex-col justify-between items-center min-w-max">
<DialogTitle>
<CheckCircleIcon class="w-12 h-12 text-success" />
</DialogTitle>
<DialogDescription as="section"
class="flex-auto self-stretch my-10 rounded p-2 break-all max-w-4xl">
{{ store.SuccessMessage }}
</DialogDescription>
<button @click="setIsOpen(false)"
class="btn btn-primary rounded-xl">OK</button>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { publicStore } from '@/stores/public'
import {
Dialog,
DialogPanel,
DialogTitle,
DialogDescription,
TransitionChild,
TransitionRoot
} from '@headlessui/vue'
import {
ExclamationCircleIcon
} from "@heroicons/vue/outline"
import { onMounted, ref, onBeforeMount } from 'vue';
const store = publicStore()
function setIsOpen() {
store.warningFn()
store.isWarn = false
}
function setIsOpenCancel() {
store.isWarn = false
}
</script>
<template>
<TransitionRoot as="template" :show="true">
<Dialog @close="setIsOpenCancel" class="min-w-max min-h-max z-20">
<TransitionChild enter="transition-opacity duration-300" enter-from="opacity-0" enter-to="opacity-100"
leave="transition-opacity duration-300" leave-from="opacity-100" leave-to="opacity-0">
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 grid place-items-center min-w-max">
<TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100" leave="duration-200 ease-in" leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95">
<DialogPanel
class=" w-1/3 h-1/2 bg-base-100 p-10 rounded-xl shadow-xl flex flex-col justify-between items-center min-w-max">
<DialogTitle>
<ExclamationCircleIcon class="w-12 h-12 text-warning" />
</DialogTitle>
<DialogDescription
class="flex-auto self-stretch my-10 rounded p-2 break-all max-w-4xl overflow-auto"
>
{{store.warnMessage}}
</DialogDescription>
<div class="flex justify-evenly w-full">
<button @click="setIsOpen"
class="btn btn-primary rounded-xl">OK</button>
<button @click="setIsOpenCancel"
class="btn btn-primary rounded-xl">Cancel</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import {
Combobox, ComboboxButton, ComboboxInput, ComboboxOptions, ComboboxOption, ComboboxLabel,
} from "@headlessui/vue"
import { SelectorIcon } from "@heroicons/vue/outline"
/**
*
* @zh 之后优化的时候可以把autoComplete的input做成伸缩式,可以由组件去
*/
const {
optionItems,
inputFn,
blurFn = _=>{},
optionsInit=(_)=>{},
filterFn,
supportedover=false
} = defineProps<{
label: string,
optionItems: Item[],
optionsInit?:(event:FocusEvent)=>void,
filterFn?:(query:string,item:Item)=>boolean
inputFn?: (value:string) => Promise<unknown>
blurFn?:(value:any)=>void
supportedover?:boolean
}>()
const query = ref('')
const selectedItem = ref({name:"",value:""} as Item)
const filterItems = computed(() => {
let result:Item[] = []
if(query.value === ""){
selectedItem.value = {name:"",value:""}
result = optionItems
} else {
result = optionItems.filter(item=>{
if(filterFn) return filterFn(query.value,item)
else {
return item.name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
}
})
}
if(supportedover) result.unshift(selectedItem.value)
return result
})
let changeMutex = true
const changeF = (event:Event &{target:HTMLInputElement}) => {
query.value = event.target.value
if(changeMutex) {
changeMutex = false
if(inputFn) inputFn(query.value).finally(()=>changeMutex = true)
else changeMutex = false
}
}
const blurF = (event:Event)=>{
blurFn(selectedItem.value.value)
}
</script>
<template>
<Combobox v-model="selectedItem" class="flex items-center" as="div">
<ComboboxLabel class="p-2">{{ label }}</ComboboxLabel>
<div class="relative flex-1">
<div
class="relative w-full cursor-default
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline
outline-2
min-w-[15rem]
hover:shadow-md transition">
<ComboboxInput class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none" @change="changeF" @focus.prevent="optionsInit" @blur="blurF"
:displayValue="(item) => (item as Item).name" />
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
<SelectorIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxButton>
</div>
<ComboboxOptions
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div v-if="filterItems.length === 0 && query !== ''"
class="relative cursor-default select-none py-2 px-4 text-gray-700">
Nothing found.
</div>
<ComboboxOption
v-for="(item,i) in filterItems" as="template" :key="i" :value="item"
v-slot="{ selected, active }">
<li class="relative cursor-default select-none p-2" :class="{
'bg-blue-400 text-white': active,
'bg-blue-600 text-white': selected,
'text-gray-900': !active && !selected,
}">
<span class="block truncate"
:class="{ 'font-medium': selected, 'font-normal': !selected, 'text-white': active, 'text-teal-600': !active && !selected }">
{{ item.name }}
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
<slot :selectItem="selectedItem"></slot>
</Combobox>
</template>
<style scoped>
</style>
@@ -0,0 +1,89 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { fetchStore } from '@/stores/fetch';
import { ref } from 'vue';
import { interpret } from 'xstate';
import AutoComplete from './AutoComplete.vue';
const { label = "className", supportedover = false, noClassloader=false } = defineProps<{
label?: string,
submitF: (data: {
classItem: Item,
loaderItem: Item
}) => void
supportedover?:boolean
noClassloader?:boolean
}>()
const optionClass = ref([] as { name: string, value: string }[])
const optionClassloders = ref([] as { name: string, value: string }[])
const fetchS = fetchStore()
// const selectedClassItem = ref({ name: "", value: "" } as Item)
const changeValue = (value: string) => {
const searchClass = interpret(permachine)
if (value.length > 2) {
return fetchS.baseSubmit(searchClass, {
action: "exec",
command: `sc *${value}*`
}).then(
res => {
optionClass.value.length = 0
let result = (res as CommonRes).body.results[0]
if (result.type === "sc" && !result.detailed && !result.withField) {
console.log(result)
result.classNames.forEach(name => {
optionClass.value.push({
name,
value: name
})
})
}
}
)
}
return Promise.resolve()
}
const blurF = (value: unknown) => {
if (value !== "" && !noClassloader) {
const searchClass = interpret(permachine)
fetchS.baseSubmit(searchClass, {
action: "exec",
command: `sc -d *${value}*`
}).then(
res => {
let result = (res as CommonRes).body.results[0]
if (result.type === "sc" && result.detailed) {
optionClassloders.value = result.classInfo.classloader.map(v => ({
name: v,
value: v.split("@")[1]
}))
optionClassloders.value.unshift({
name: "default",
value: ""
})
}
}
)
}
}
const filterfn = (_: any, item: Item) => true
</script>
<template>
<AutoComplete :label="label" :option-items="optionClass" :input-fn="changeValue" :filter-fn="filterfn" v-slot="slotP" :supportedover="supportedover"
:blur-fn="blurF" as="form">
<template v-if="noClassloader">
<slot name="others"></slot>
<button @click.prevent="submitF({
classItem:slotP.selectItem,
loaderItem:slotP.selectItem
})" class="btn btn-primary btn-sm btn-outline mx-2 transition">submit</button>
</template>
<AutoComplete label="classloader" :option-items="optionClassloders" v-slot="slotQ" v-else>
<slot name="others"></slot>
<button @click.prevent="submitF({
classItem:slotP.selectItem,
loaderItem:slotQ.selectItem,
})" class="btn btn-primary btn-sm btn-outline mx-2 transition">submit</button>
</AutoComplete>
</AutoComplete>
</template>
@@ -0,0 +1,112 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import { ref } from 'vue';
import { interpret } from 'xstate';
import AutoComplete from './AutoComplete.vue';
const { label = "className", ncondition = false, nexpress = false, ncount = false } = defineProps<{
label?: string,
nexpress?: boolean,
ncondition?: boolean,
ncount?: boolean,
submitF: (data: {
classItem: Item, methodItem: Item,
conditon: string,
express: string,
count: number
}) => void
}>()
const fetchS = fetchStore()
const optionClass = ref([] as { name: string, value: string }[])
const optionMethod = ref([] as { name: string, value: string }[])
const conditon = ref("")
const express = ref("")
const autoStop = ref(0)
const changeClass = (value: string) => {
const searchClass = interpret(permachine)
if (value.length > 2) {
return fetchS.baseSubmit(searchClass, {
action: "exec",
command: `sc *${value}*`
}).then(
res => {
optionClass.value.length = 0
let result = (res as CommonRes).body.results[0]
if (result.type === "sc" && !result.detailed && !result.withField) {
result.classNames.forEach(name => {
optionClass.value.push({
name,
value: name
})
})
}
}
)
}
return Promise.resolve()
}
const changeMethod = (classV: string, value: string) => {
const searchMethod = interpret(permachine)
return fetchS.baseSubmit(searchMethod, {
action: "exec",
command: `sm ${classV} *${value}*`
}).then(
res => {
optionMethod.value.length = 0
; (res as CommonRes).body.results.forEach(result => {
if (result.type === "sm") {
const name = result.methodInfo.methodName
optionMethod.value.push({
name,
value: name
})
}
})
}
)
}
const setCount = publicStore().inputDialogFactory(autoStop,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 0 : valRaw
},
(input) => input.value.toString()
)
const setConditon = publicStore().inputDialogFactory(
conditon,
raw => raw,
_ => _.value
)
const setExpress = publicStore().inputDialogFactory(
express,
raw => raw,
_ => _.value
)
const filterfn = (_: any, item: Item) => true
</script>
<template>
<AutoComplete :label="label" :option-items="optionClass" :input-fn="changeClass" :filter-fn="filterfn"
v-slot="slotClass">
<AutoComplete label="method" :option-items="optionMethod"
:input-fn="(value: string) => changeMethod(slotClass.selectItem.value as string, value)" :filter-fn="filterfn"
v-slot="slotMethod">
<button v-if="nexpress" class="btn btn-sm btn-outline ml-2" @click="setExpress">express:{{express}}</button>
<button v-if="ncondition" class="btn btn-sm btn-outline ml-2" @click="setConditon">condition:{{conditon}}</button>
<button v-if="ncount" class="btn btn-sm btn-outline ml-2" @click="setCount">count:{{autoStop}}</button>
<slot name="others" :methodItem="slotMethod.selectItem" :classItem="slotClass.selectItem"></slot>
<button @click.prevent="submitF({
classItem: slotClass.selectItem,
methodItem: slotMethod.selectItem,
conditon,
express,
count:autoStop
})" class="btn btn-primary btn-sm btn-outline mx-2 transition">submit</button>
</AutoComplete>
</AutoComplete>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Switch } from '@headlessui/vue'
import { PlayIcon, StopIcon } from "@heroicons/vue/solid"
const { playFn = () => { }, stopFn = () => { }, defaultEnabled } = defineProps<{
playFn?: Function,
stopFn?: Function,
defaultEnabled: boolean
}>()
const enabled = ref(defaultEnabled)
const toggle = () => enabled.value ? stopFn() : playFn()
</script>
<template>
<Switch v-model="enabled"
class="rounded-full grid place-items-center hover:opacity-50 transition" @click="toggle">
<PlayIcon v-if="!enabled" class="w-full h-full text-blue-500" />
<StopIcon v-else class="w-full h-full text-red-500" />
</Switch>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { onBeforeMount, reactive, ref, watch, watchEffect } from 'vue';
import {
Switch
} from "@headlessui/vue"
const props = defineProps<{
data: { key: string, value: boolean | string },
send: Function
}>()
const modelvalue = ref('' as string | boolean)
watch(props, () => {
modelvalue.value = props.data.value
console.log("watch", props.data)
}, {
deep: true,
immediate: true
})
onBeforeMount(() => {
console.log(props.data)
})
</script>
<template>
<form class="flex justify-between items-center" @submit.prevent="send({ key: data.key, value: modelvalue })">
<div class="w-1/5 bg-blue-200 p-2 min-w-max">{{ data.key }}</div>
<div class="w-3/5 grid place-items-center">
<Switch v-if="typeof data.value === 'boolean'" v-model="(modelvalue as boolean)"
:class="modelvalue ? 'bg-blue-500' : 'bg-teal-700'"
class="relative inline-flex h-6 w-11 items-center rounded-full">
<span class="sr-only">Enable notifications</span>
<span :class="modelvalue ? 'translate-x-6' : 'translate-x-1'"
class="inline-block h-4 w-4 transform rounded-full bg-white transition" />
</Switch>
<input v-else-if="typeof data.value === 'string'" v-model="(modelvalue as string)" class="rounded-full pl-3 w-1/2"/>
</div>
<div class="w-1/5">
<button
class="p-1 w-24 rounded-full bg-blue-300 hover:bg-blue-500 transition text-black">
change
</button>
</div>
</form>
</template>
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { PlusCircleIcon, MinusCircleIcon } from "@heroicons/vue/outline"
import {
Menu,
MenuButton,
MenuItem,
MenuItems
} from "@headlessui/vue"
import { ref } from "vue";
import { publicStore } from "@/stores/public";
const { valSet = new Set<string>(), getInput = () => true } = defineProps<{
valSet?: Set<string>,
getInput?: (raw: string) => boolean,
title: string
}>()
const publicS = publicStore()
const openInput = publicS.inputDialogFactory(
ref(""),
(raw) => {
if (getInput(raw)) valSet.add(raw)
return ""
},
_ => ""
)
const removeValSet = (val: string, valSet: Set<string>) => {
valSet.delete(val)
}
</script>
<template>
<Menu as="div" class=" relative flex items-center">
<MenuButton class=" w-52 hover:shadow-md btn btn-sm btn-outline">{{title}}</MenuButton>
<MenuItems
class=" absolute w-52 mt-2 border py-2 rounded-md hover:shadow-xl transition bg-base-100 max-h-80 overflow-y-auto top-[100%]">
<MenuItem v-slot="{active}">
<li class="flex justify-center " :class='{" bg-neutral text-neutral-content": active}'>
<PlusCircleIcon class="w-6 h-6 cursor-pointer" @click="openInput"></PlusCircleIcon>
</li>
</MenuItem>
<template v-if="valSet.size > 0">
<MenuItem v-slot="{ active, selected }" v-for="(v) in valSet.values()" :key="v">
<li class="flex w-full justify-between px-2" :class='{"bg-neutral text-neutral-content":
active, "bg-neutral-focus text-neutral-content" : selected,}'>
<div>{{v}}</div>
<MinusCircleIcon class="w-6 h-6 cursor-pointer" @click="removeValSet(v,valSet)"></MinusCircleIcon>
</li>
</MenuItem>
</template>
</MenuItems>
</Menu>
</template>
<style scoped>
</style>
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { PuzzleIcon, TerminalIcon, ViewGridIcon } from "@heroicons/vue/outline"
import { DesktopComputerIcon } from "@heroicons/vue/solid"
import { useRoute, useRouter } from 'vue-router';
const tabs = [
{
name: 'dashboard',
url: "/dashboard",
icon: DesktopComputerIcon
},
{
name: 'immediacy',
url: '/synchronize',
icon: ViewGridIcon
}, {
name: "real time",
url: '/asynchronize',
icon: ViewGridIcon
},
{
name: 'option',
url: '/config',
icon: PuzzleIcon
},
{
name: 'console',
url: '/console',
icon: TerminalIcon
},
]
const router = useRouter()
const routePath = computed(() => useRoute().path)
const toNext = (url: string) => {
router.push(url)
}
const a: StatusResult = { type: "status", message: "", statusCode: 0 }
</script>
<template>
<!-- <div class=" h-full bg-gray-300">
<ul class="flex flex-col justify-start w-40 h-full items-stretch bg-blue-50">
<li v-for="(tab, idx) in tabs" :key="idx" class="flex justify-center items-center hover:bg-gray-200 transition"
@click="toNext(tab.url)" :class="{ 'bg-gray-200': routePath.includes(tab.url), }">
<div class="bg-gray-200 h-10 w-10 grid place-items-center rounded-full">
<component :is="tab.icon" class="w-3/4 h-3/4 text-gray-500" />
</div>
<button class=" outline-none grid place-items-centerh-16 w-20 m-4">{{
tab.name
}}</button>
</li>
</ul>
<ul class=" w-0 overflow-hidden">
<slot name="detail">
</slot>
</ul>
</div> -->
<ul class="menu bg-base-200 w-[10vw] menu-compact">
<li v-for="(tab, idx) in tabs" :key="idx"
@click="toNext(tab.url)" class="pl-2" >
<a class="break-all" :class="{ 'bg-primary text-primary-content': routePath.includes(tab.url), }">
<component :is="tab.icon" class="w-4 h-4" />
{{
tab.name
}}
</a>
</li>
</ul>
</template>
<style scoped>
</style>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
defineProps<{
routes: { cmd: string, url: string }[]
}>()
const router = useRouter()
const route = useRoute()
const routePath = computed(() => route.path)
</script>
<template>
<!-- <ul class=" w-40 border-l bg-gray-200 ">
<li v-for="(v, i) in routes" :key="i">
<button @click="() => router.push(v.url)"
class=" transition w-full h-12 bg-gray-300 hover:bg-slate-400"
:class='{"bg-gray-500 text-white":routePath.includes(v.url)}'
>
{{ v.cmd }}
</button>
</li>
</ul> -->
<ul class=" w-[10vw] menu bg-base-300 menu-compact">
<li v-for="(v, i) in routes" :key="i">
<a @click="() => router.push(v.url)" :class='{"bg-secondary text-secondary-content":routePath.includes(v.url)}'>
{{ v.cmd }}
</a>
</li>
</ul>
</template>
<style scoped>
</style>
@@ -0,0 +1,57 @@
<script setup lang="ts">
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from "@headlessui/vue"
// bug??? data = title就会暴毙
const { title, map, buttonWidth = 'w-80', open = false, data = "", buttonAccent=false } = defineProps<{
title: string,
open?: boolean
// reative Proxy
map: Map<string, string[]>,
buttonWidth?: string,
buttonAccent?:boolean
data?: any
}>()
const emit = defineEmits(["myclick"])
const disposeClick = (e:Event)=>{
emit('myclick',e)
}
</script>
<template>
<Disclosure as="section" class="w-100 flex flex-col mb-2">
<DisclosureButton
@click.prevent="disposeClick"
class="bg-info py-1 text-info-content rounded truncate"
:class="{'bg-accent text-accent-content':buttonAccent,[buttonWidth]:true}"
>
{{ title }}
</DisclosureButton>
<transition enter-active-class="transition duration-75 ease-out" enter-from-class="h-0 opacity-0"
enter-to-class="h-auto opacity-100" leave-active-class="transition duration-75 ease-out"
leave-from-class="h-auto opacity-100" leave-to-class="h-0 opacity-0">
<DisclosurePanel class="text-gray-500 w-full" as="ul" :static="open">
<slot name="headerAside" :data="data"></slot>
<div v-for="([k, v], i) in map" :key="k" class="flex mt-1">
<Disclosure>
<DisclosureButton class="bg-base-300 text-base-content w-40 break-all flex-shrink-0">
{{ k }}
</DisclosureButton>
<DisclosurePanel as="ul" static class="flex-auto bg-base-200 text-base-content flex flex-col justify-center">
<li v-for="(cv, ci) in v" :key="ci" :class="{ 'border-t-2': (ci > 0), 'border-base-100': (ci > 0) }"
class=" pl-2 break-all">
{{ cv }}
</li>
</DisclosurePanel>
</Disclosure>
</div>
<slot name="others"></slot>
</DisclosurePanel>
</transition>
</Disclosure>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
const { result } = defineProps<{
result: EnchanceResult
}>()
let enhancer = new Map<string, string>()
if (result.type === "enhancer") {
enhancer.clear();
enhancer.set("success", result.success.toString());
for (const k in result.effect) {
enhancer.set(k, result.effect[k as "cost"].toString());
}
}
</script>
<template>
<!-- <div class="flex items-center w-full justify-center flex-col"> -->
<!-- <h3 class=" text-2xl mb-2">enchance</h3> -->
<!-- <ul class="flex">
<li
v-for="(kv,i) in enhancer.entries()" :key="i"
class="flex flex-col mx-1">
<div class="bg-orange-300 p-2 rounded-b">{{kv[0]}}</div>
<div class=" bg-orange-100 grid place-items-center">{{kv[1]}}</div>
</li>
</ul> -->
<!-- <table class="table table-compact group w-full">
<thead>
<tr>
<th v-for="(k,i) in enhancer.keys()" :key="i" class='border border-slate-300'
:class="{'group-first:z-0':i==0}">{{k}}</th>
</tr>
</thead>
<tbody>
<tr>
<td v-for="(v, i) in enhancer.values()" class="border border-slate-300">
{{v}}
</td>
</tr>
</tbody>
</table> -->
<div class="stats shadow my-2 mx-auto">
<div class="stat place-items-center" v-for="(kv,i) in enhancer.entries()" :key="i" >
<div class="stat-title">{{kv[0]}}</div>
<div class="stat-value">{{kv[1]}}</div>
</div>
</div>
<!-- </div> -->
</template>
<style scoped>
</style>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import {
Disclosure,
DisclosureButton,
DisclosurePanel
} from "@headlessui/vue"
// import SelectInput from "./SelectInput.vue";
defineProps<{
title: string,
list: { [x: string]: any }[],
titleKeyName: string
}>()
</script>
<template>
<Disclosure as="section" class="w-100 flex flex-col mb-2">
<DisclosureButton class="text-info-content py-1 bg-info rounded self-start w-80 ">
{{ title }}
</DisclosureButton>
<transition enter-active-class="transition duration-75 ease-out" enter-from-class="h-0 opacity-0"
enter-to-class="h-auto opacity-100" leave-active-class="transition duration-75 ease-out"
leave-from-class="h-auto opacity-100" leave-to-class="h-0 opacity-0">
<DisclosurePanel class=" w-10/12" as="ul">
<li v-for="(v, i) in list" :key="i" class="flex mt-1">
<Disclosure>
<DisclosureButton class="bg-base-300 w-1/4 p2 break-all">
{{ v[titleKeyName] }}
</DisclosureButton>
<DisclosurePanel as="div" static class="flex-auto bg-base-200 flex flex-col justify-center ml-1">
<template v-for="(cv, ci) in Object.entries(v).filter(n => n[0] !== titleKeyName)" :key="ci">
<slot name="item" :kv="cv" :itemTitle="(v[titleKeyName] as string)" :idx="ci"></slot>
</template>
</DisclosurePanel>
</Disclosure>
</li>
</DisclosurePanel>
</transition>
</Disclosure>
</template>
<style scoped>
</style>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref, toRefs } from 'vue';
const _props = defineProps<{
maplist:Map<string, string>[]
}>()
const props = toRefs(_props)
const hlist= ref([] as string[])
if(props.maplist.value.length > 0) {
console.dir(hlist)
hlist.value = Object.keys(props.maplist.value[0])
}
</script>
<template>
<table class="border-collapse border border-slate-400 ...">
<thead>
<tr>
<th class="border border-slate-300 ..." v-for="(v,i) in hlist" :key="i"></th>
</tr>
</thead>
<tbody>
<tr v-for="(map, i) in maplist" :key="i">
<td class="border border-slate-300 ..." v-for="(key,j) in hlist" :key="j">{{map.get(key)}}</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
</style>
@@ -0,0 +1,53 @@
<script setup lang="ts">
// @ts-nocheck
// 忽略文件报错
import {
getCurrentInstance,
onMounted,
ref
} from "vue"
import {
Disclosure,
DisclosurePanel,
DisclosureButton
} from "@headlessui/vue"
const { root, classList = "", buttonClass = '' } = defineProps<{
root: TreeNode
classList?: string[] | Record<string, boolean> | string,
buttonClass?: string[] | string
}>()
const btn = ref(null)
onMounted(() => {
(btn.value.el as HTMLButtonElement).dispatchEvent(new Event("click"))
})
</script>
<template>
<div :class="classList" v-if="root">
<Disclosure>
<div class="flex items-center mb-1 group">
<DisclosureButton :class="buttonClass" ref="btn">
<slot name="meta" :data="root.meta" :active="root.children.length > 0"></slot>
</DisclosureButton>
<slot name="others" :data="root.meta" ></slot>
</div>
<template v-if='root.children !== undefined && root.children.length > 0'>
<DisclosurePanel class="pl-4 border-l border-black">
<Tree v-for="(child, i) in root.children" :key="i" :root="child" :class-list="classList">
<template #meta="{data, active}">
<slot name="meta" :data="data" :active="active"></slot>
</template>
<template #others="{data}">
<slot name="others" :data="data" ></slot>
</template>
</Tree>
</DisclosurePanel>
</template>
</Disclosure>
</div>
</template>
<style scoped>
</style>
+16
View File
@@ -0,0 +1,16 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+12
View File
@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.button-style {
@apply bg-blue-400 hover:opacity-50 transition rounded-md p-2
}
.input-btn-style {
@apply border p-2 rounded-xl hover:shadow-md transition border-gray-300
}
}
@@ -0,0 +1,483 @@
import { assign, createMachine, DoneInvokeEvent, spawn } from "xstate";
import { fetchStore } from "@/stores/fetch";
import { publicStore } from "@/stores/public";
import transformMachine from "./transformConfigMachine";
interface CTX {
toObjM: typeof transformMachine | null;
inputRaw: any;
inputValue?: ArthasReq;
request?: Request;
response?: ArthasRes;
resArr: (ArthasResResult | SessionRes | AsyncRes)[];
err: string;
// 暂时先anyscript
publicStore?: any;
fetchStore?: any;
}
type ET =
| {
type: "SUBMIT";
value: ArthasReq | string;
}
| {
type: "error.platform";
data: any;
}
| {
type: "done.invoke.getCommon";
data: CommonRes;
}
| {
type: "done.invoke.getSession";
data: SessionRes;
}
| {
type: "done.invoke.getAsync";
data: AsyncRes;
}
| {
type: "INIT";
}
// | {
// type: "CLEAR_RESARR";
// }
| {
type: "";
};
const machine =
/** @xstate-layout N4IgpgJg5mDOIC5QGMD2A7WqA2YB0AlhLgMQDKAqgEICyAkgCqKgAOqsBALgRsyAB6IAjAHYALHiEBmAKxiADADYATIoAcytaLEAaEAE9hQgJx5FQtWMUzjU+cvlq1AX2d60mHPmyoAhhAJ0KBIIDHxAgDdUAGt8DyxcPB9-QKgESNRkX24MAG15AF0+Ng4c9D5BBBl5ITNZMREZRUVjeWMtPUMEIXs8Y0UxLRF5KUtBmVd3DATvPwCgkjAAJyXUJbwWbGyAMzWAWzx4ryS51PT0KKyy-KKkEBKuHnK7yuraxXrG5tb2oU7EZSAvBOZrKERCGzKQbGESuNwgdCoCBwPhHRJEXDFdiPXgvRBiZT-bpSKSSeT2KRfKHGMQTeFo2YpIJY0pPCoAyx4OwNCEyERqeQKCFEnoiMzU8RiGFyRpSSYgBl4WAAV2QyDg8DuDzK7IQ6hkeChgLUUmUFkBLSJrT64KkihEgKU9rUsPp02O218BGwyqWYBZOOeoEq+sNBM0pvNqmMRLN8mBtlEIikYiEVhNrqmnkxWuxOrx3TUIrUeHJZfLFddriAA */
createMachine({
context: {
toObjM: null,
inputRaw: undefined,
inputValue: undefined,
request: undefined,
response: undefined,
publicStore: undefined,
fetchStore: undefined,
resArr: [],
err: "",
},
schema: {
context: {} as CTX,
events: {} as ET,
services: {} as {
requestData: { data: any };
stringToObj: { data: any };
},
},
id: "console",
initial: "idle",
states: {
idle: {
on: {
// 延迟pinia的挂载时机
INIT: {
target: "ready",
actions: "initStore",
},
},
},
ready: {
initial: "stringVal",
states: {
stringVal: {
on: {
SUBMIT: [{
cond: "notString",
actions: "rawInput",
target: "objVal",
}, {
actions: [
"rawInput",
"toObj",
],
target: "objVal",
}],
},
},
objVal: {
entry: assign<CTX, ET>((ctx, e) => {
return {
inputValue: ctx.inputRaw,
};
}),
always: [
{ cond: "notObj", target: "#failure" },
{
cond: "isAsync",
target: "#asyncReq",
},
{
cond: "isCommon",
target: "#common",
},
{
cond: "isSession",
target: "#session",
},
{
actions: "notReq",
target: "#failure",
},
],
exit: "getReq",
},
},
},
common: {
id: "common",
tags: ["loading"],
entry: "waitReq",
invoke: {
id: "getCommon",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
states: {},
},
session: {
id: "session",
tags: ["loading"],
invoke: {
id: "getSession",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformSessionRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
},
asyncReq: {
id: "asyncReq",
tags: ["loading"],
entry: "waitReq",
invoke: {
id: "getAsync",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformAsyncRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
},
success: {
entry: ["needReportSuccess", "renderRes"],
tags: "result",
always: {
actions: ["reset"],
target: "ready",
},
},
failure: {
id: "failure",
tags: "result",
entry: "outputErr",
always: {
actions: ["reset"],
target: "ready",
},
},
hist: {
type: "history",
},
},
}, {
services: {
requestData: async (context) => {
const res = await fetch(context.request as Request);
if (!res.ok) return Promise.reject("server error");
return res.json();
},
},
actions: {
initStore: assign((context, event) => {
if (event.type !== "INIT") return {};
return {
publicStore: publicStore(),
fetchStore: fetchStore(),
};
}),
rawInput: assign((context, event) => {
if (event.type !== "SUBMIT") return {};
return {
inputRaw: event.value,
};
}),
waitReq: (context) => {
context.fetchStore.onWait();
},
getReq: assign((context, event) => {
if (
!context.inputValue || !context.fetchStore ||
!("getRequest" in context.fetchStore)
) {
return {};
}
return {
request: context.fetchStore?.getRequest(context.inputValue),
inputValue: context.inputValue,
};
}),
transformRes: assign((context, event) => {
if (event.type !== "done.invoke.getCommon") return {};
return {
response: event.data,
};
}),
transformSessionRes: assign((context, event) => {
if (event.type !== "done.invoke.getSession") return {};
return {
response: event.data,
};
}),
transformAsyncRes: assign((ctx, e) => {
if (e.type !== "done.invoke.getAsync") return {};
return {
response: e.data,
};
}),
renderRes: assign((context, event) => {
let resArr: (ArthasResResult | SessionRes | AsyncRes)[] =
context.resArr;
const response = context.response;
if (!response) {
return {};
}
if (
Object.hasOwn(response, "body") &&
Object.hasOwn((response as CommonRes).body, "results")
) {
resArr = resArr.concat((context.response as CommonRes).body.results);
} else {
resArr = resArr.concat([response] as (SessionRes | AsyncRes)[]);
}
return { resArr: resArr.filter((v) => v && !Number.isNaN(v)) };
}),
outputErr: assign({
err: (context, event) => {
context.publicStore.$patch({
isErr: true,
ErrMessage: context.err,
});
return "";
},
}),
// clearResArr: assign((context, event) => ({ resArr: [] })),
toObj: assign((ctx) => {
const m = spawn(transformMachine, { sync: true });
m.send({
type: "INPUT",
data: ctx.inputRaw as string,
});
const s = m.getSnapshot();
if (s?.matches("failure")) {
return {
inputRaw: undefined,
err: s.context.err,
};
}
let inputRaw:ArthasReq = {
sessionId:undefined,
...s?.context.output
} as ArthasReq
return {
inputRaw,
};
}),
needReportSuccess: (context, e) => {
if (context.inputValue?.action === "close_session") {
context.fetchStore.$patch({
sessionId: "",
consumerId: "",
online: false,
});
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: `close session success!`,
});
return;
}
if (context.inputValue?.action === "init_session") {
const response = (context.response as SessionRes);
context.fetchStore.$patch({
sessionId: response.sessionId,
consumerId: response.consumerId,
online: true,
});
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: `init_session success!`,
});
return;
}
if (
context.inputValue?.action === "exec" &&
context.inputValue.command.includes("vmoption") &&
context.inputValue.command !== "vmoption"
) {
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: JSON.stringify(
(context.response as CommonRes).body.results,
),
});
return;
}
},
setErrMessage: assign((_ctx, e) => {
if (e.type === "SUBMIT" || e.type === "INIT" || e.type === "") {
return {};
}
return { err: e.data as unknown as string };
}),
reset: (ctx, e) => {
ctx.fetchStore.waitDone();
},
notReq: assign((context) => {
return {
err: "not request",
};
}),
},
guards: {
// 判断命令是否有问题
cmdSucceeded: (context, event) => {
if (
event.type !== "done.invoke.getCommon" &&
event.type !== "done.invoke.getSession" &&
event.type !== "done.invoke.getAsync"
) {
return false;
}
if (["SCHEDULED", "SUCCEEDED"].includes(event.data.state)) {
if (Object.hasOwn(event.data, "body")) {
if (Object.hasOwn(event.data.body, "results")) {
return (event.data as CommonRes).body.results.every((result) => {
if (result.type === "status" && result.statusCode !== 0) {
return false;
}
if (
result.type === "message" &&
result.message ===
"all consumers are unhealthy, current job was interrupted."
) {
return false;
}
return true;
});
} else {
return ["READY", "TERMINATED"].includes(
(event.data as AsyncRes).body.jobStatus,
);
}
}
// SessionRes
return true;
}
if(context.inputValue && context.inputValue.action=== "interrupt_job") {
/**
* 永不拦截打断回收的错误
*/
return true
}
return false;
},
isSession: (context) => {
if (!context) return false;
if (
["join_session", "init_session", "close_session", "interrupt_job"]
.includes(context.inputValue!.action)
) {
console.log("isSession");
return true;
}
return false;
},
isCommon: (context) => {
if (!context) return false;
if (
["exec", "pull_results"]
.includes(context.inputValue!.action)
) {
console.log("isCommon");
return true;
}
return false;
},
isAsync: (context) => {
if (!context) return false;
if (
["async_exec"]
.includes(context.inputValue!.action)
) {
console.log("isAsync");
return true;
}
return false;
},
notObj: (ctx) => {
if (ctx.inputValue) return false;
return true;
},
// notReq: (context) => {
// if (context.inputValue) return true;
// return false;
// },
notString: (context, event) => {
if (event.type !== "SUBMIT") return true;
if (typeof event.value !== "string") return true;
return false;
},
},
});
export default machine;
@@ -0,0 +1,462 @@
import { assign, createMachine, DoneInvokeEvent, spawn } from "xstate";
import { fetchStore } from "@/stores/fetch";
import { publicStore } from "@/stores/public";
import transformMachine from "./transformConfigMachine";
interface CTX {
toObjM: typeof transformMachine | null;
inputRaw: any;
inputValue?: ArthasReq;
request?: Request;
response?: ArthasRes;
err: string;
// 暂时先anyscript
publicStore?: any;
fetchStore?: any;
}
type ET =
| {
type: "SUBMIT";
value: ArthasReq | string;
}
| {
type: "error.platform";
data: any;
}
| {
type: "done.invoke.getCommon";
data: CommonRes;
}
| {
type: "done.invoke.getSession";
data: SessionRes;
}
| {
type: "done.invoke.getAsync";
data: AsyncRes;
}
| {
type: "INIT";
}
| {
type: "";
};
const permachine = createMachine({
context: {
toObjM: null,
inputRaw: undefined,
inputValue: undefined,
request: undefined,
response: undefined,
publicStore: undefined,
fetchStore: undefined,
err: "",
},
schema: {
context: {} as CTX,
events: {} as ET,
services: {} as {
requestData: { data: any };
stringToObj: { data: any };
},
},
id: "console",
initial: "idle",
states: {
idle: {
on: {
// 延迟pinia的挂载时机
INIT: {
target: "ready",
actions: "initStore",
},
},
},
ready: {
initial: "stringVal",
states: {
stringVal: {
on: {
SUBMIT: [{
cond: "notString",
actions: "rawInput",
target: "objVal",
}, {
actions: [
"rawInput",
"toObj",
],
target: "objVal",
}],
},
},
objVal: {
entry: assign<CTX, ET>((ctx, e) => {
return {
inputValue: ctx.inputRaw,
};
}),
always: [
{ cond: "notObj", target: "#failure" },
{
cond: "isAsync",
target: "#asyncReq",
},
{
cond: "isCommon",
target: "#common",
},
{
cond: "isSession",
target: "#session",
},
{
actions: "notReq",
target: "#failure",
},
],
exit: "getReq",
},
},
},
common: {
id: "common",
tags: ["loading"],
entry: "waitReq",
invoke: {
id: "getCommon",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
},
session: {
id: "session",
tags: ["loading"],
invoke: {
id: "getSession",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformSessionRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
},
asyncReq: {
id: "asyncReq",
tags: ["loading"],
entry: "waitReq",
invoke: {
id: "getAsync",
src: "requestData",
onDone: [
{
cond: "cmdSucceeded",
actions: ["transformAsyncRes"],
target: "success",
},
{
actions: ["setErrMessage"],
target: "failure",
},
],
onError: [
{
actions: ["setErrMessage"],
target: "failure",
},
],
},
},
success: {
entry: ["needReportSuccess", "reset"],
type: "final",
tags: "result",
},
failure: {
id: "failure",
type: "final",
tags: "result",
entry: ["outputErr", "reset"],
},
hist: {
type: "history",
},
},
}, {
services: {
requestData: async (context) => {
const res = await fetch(context.request as Request);
if (!res.ok) return Promise.reject("server error");
return res.json();
},
},
actions: {
initStore: assign((context, event) => {
if (event.type !== "INIT") return {};
return {
publicStore: publicStore(),
fetchStore: fetchStore(),
};
}),
rawInput: assign((context, event) => {
if (event.type !== "SUBMIT") return {};
return {
inputRaw: event.value,
};
}),
waitReq: (context) => {
context.fetchStore.onWait();
},
getReq: assign((context, event) => {
if (
!context.inputValue || !context.fetchStore ||
!("getRequest" in context.fetchStore)
) {
return {};
}
/**
* session的never和undefinded让fetch.ts来控制
*/
return {
request: context.fetchStore?.getRequest(context.inputValue),
inputValue: context.inputValue as ArthasReq,
};
}),
transformRes: assign({
response: (context, event) => {
if (event.type !== "done.invoke.getCommon") return undefined;
return event.data;
},
}),
transformSessionRes: assign((context, event) => {
if (event.type !== "done.invoke.getSession") return {};
return {
response: event.data,
};
}),
transformAsyncRes: assign((ctx, e) => {
if (e.type !== "done.invoke.getAsync") return {};
return {
response: e.data,
};
}),
outputErr: assign((context, event) => {
if (!context.publicStore.ignore) {
context.publicStore.$patch({
isErr: true,
ErrMessage: context.err,
});
} else{
console.error(context.err)
}
return {
err: "",
};
}),
// clearResArr: assign((context, event) => ({ resArr: [] })),
toObj: assign((ctx) => {
const m = spawn(transformMachine, { sync: true });
m.send({
type: "INPUT",
data: ctx.inputRaw as string,
});
const s = m.getSnapshot();
if (s?.matches("failure")) {
return {
inputRaw: undefined,
err: s.context.err,
};
}
return {
inputRaw: s?.context.output as ArthasReq,
};
}),
needReportSuccess: (context, e) => {
if (context.inputValue?.action === "close_session") {
context.fetchStore.$patch({
sessionId: "",
consumerId: "",
online: false,
});
if(context.publicStore.ignore) return;
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: `close session success!`,
});
return;
}
if (context.inputValue?.action === "init_session") {
const response = (context.response as SessionRes);
context.fetchStore.$patch({
sessionId: response.sessionId,
consumerId: response.consumerId,
online: true,
});
if(context.publicStore.ignore) return;
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: `init_session success!`,
});
return;
}
if (
(context.inputValue?.action === "exec" ||
context.inputValue?.action === "async_exec") &&
context.inputValue.command.search("profiler") >= 0
) {
let result = (context.response as CommonRes).body.results[0];
if (
result.type === "profiler" &&
["start", "resume", "stop"].includes(result.action)
) {
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: result.executeResult,
});
}
return;
}
if (
context.inputValue?.action === "exec" &&
context.inputValue.command.includes("vmoption") &&
context.inputValue.command !== "vmoption"
) {
context.publicStore.$patch({
isSuccess: true,
SuccessMessage: JSON.stringify(
(context.response as CommonRes).body.results,
),
});
return;
}
},
setErrMessage: assign((_ctx, e) => {
if (e.type === "SUBMIT" || e.type === "INIT" || e.type === "") {
return {};
}
return { err: e.data as unknown as string };
}),
reset: (ctx, e) => {
ctx.fetchStore.waitDone();
},
notReq: assign((context) => {
return {
err: "not request",
};
}),
},
guards: {
// 判断命令是否有问题
cmdSucceeded: (context, event) => {
if (
event.type !== "done.invoke.getCommon" &&
event.type !== "done.invoke.getSession" &&
event.type !== "done.invoke.getAsync"
) {
return false;
}
if (["SCHEDULED", "SUCCEEDED"].includes(event.data.state)) {
if (Object.hasOwn(event.data, "body")) {
if (Object.hasOwn(event.data.body, "results")) {
return (event.data as CommonRes).body.results.every((result) => {
return result.type === "status" ? result.statusCode === 0 : true;
});
} else {
return ["READY", "TERMINATED"].includes(
(event.data as AsyncRes).body.jobStatus,
);
}
}
// SessionRes
return true;
}
if (context.inputValue && context.inputValue.action === "interrupt_job") {
/**
* 永不拦截打断回收的错误
*/
return true;
}
return false;
},
isSession: (context) => {
if (!context) return false;
if (
["join_session", "init_session", "close_session", "interrupt_job"]
.includes(context.inputValue!.action)
) {
console.log("isSession");
return true;
}
return false;
},
isCommon: (context) => {
if (!context) return false;
if (
["exec"]
.includes(context.inputValue!.action)
) {
console.log("isCommon");
return true;
}
return false;
},
isAsync: (context) => {
if (!context) return false;
if (
["async_exec", "pull_results"]
.includes(context.inputValue!.action)
) {
console.log("isAsync");
return true;
}
return false;
},
notObj: (ctx) => {
if (ctx.inputValue) return false;
return true;
},
// notReq: (context) => {
// if (context.inputValue) return true;
// return false;
// },
notString: (context, event) => {
if (event.type !== "SUBMIT") return true;
if (typeof event.value !== "string") return true;
return false;
},
},
});
export default permachine;
@@ -0,0 +1,111 @@
import { assign, createMachine } from "xstate";
import { respond } from "xstate/lib/actions";
// import { publicStore } from "@/stores/public";
type Output = object;
interface CTX {
inputValue: string;
output?: Output;
notJSON: symbol;
err: string;
// 暂时先anyscript
// publicStore?: any,
}
type ET =
| {
type: "INPUT";
data: string;
}
| {
type: "FAILURE",
data: string
}
| {
type: "SUCCESS";
data: Output,
};
const transformMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QGMD2A7WqA2YB0AlhLgMQDKAqgEICyAkgCqKgAOqsBALgRsyAB6IAjAHYALHiEBmAKxiADADYATIoAcytaLEAaEAE9hQgJx5FQtWMUzjU+cvlq1AX2d60mHPmyoAhhAJ0KBIIDHxAgDdUAGt8DyxcPB9-QKgESNRkX24MAG15AF0+Ng4c9D5BBBl5ITNZMREZRUVjeWMtPUMEIXs8Y0UxLRF5KUtBmVd3DATvPwCgkjAAJyXUJbwWbGyAMzWAWzx4ryS51PT0KKyy-KKkEBKuHnK7yuraxXrG5tb2oU7EZSAvBOZrKERCGzKQbGESuNwgdCoCBwPhHRJEXDFdiPXgvRBiZT-bpSKSSeT2KRfKHGMQTeFo2YpIJY0pPCoAyx4OwNCEyERqeQKCFEnoiMzU8RiGFyRpSSYgBl4WAAV2QyDg8DuDzK7IQ6hkeChgLUUmUFkBLSJrT64KkihEgKU9rUsPp02O218BGwyqWYBZOOeoEq+sNBM0pvNqmMRLN8mBtlEIikYiEVhNrqmnkxWuxOrx3TUIrUeHJZfLFddriAA */
createMachine(
{
id: "JSON_TO_OBJ",
schema: {
context: {} as CTX,
events: {} as ET,
},
context: {
inputValue: "???",
notJSON: Symbol(""),
output: undefined,
err: "",
},
initial: "idle",
states: {
idle: {
on: {
INPUT: [{
cond: "isString",
actions: "getVal",
target: "handle",
}, {
actions: assign(()=>({err:"not string"})) as any,
target: "failure",
}],
},
},
handle: {
always: [
{ cond: "isJSON", actions: "handleEnvJSON", target: "success" },
{
actions: assign<CTX,ET>({err:"not JSON"}),
target: "failure",
},
],
},
failure: {
tags:["result"],
entry: respond("FAILURE"),
type: "final",
},
success: {
tags:["result"],
entry:respond("SUCCESS"),
type: "final",
},
},
},
{
actions: {
getVal: assign((context, e) => {
if (e.type !== "INPUT") return {};
return {
inputValue: e.data,
};
}),
handleEnvJSON: assign((context) => {
const output = JSON.parse(context.inputValue);
return {
output,
};
}),
},
guards: {
isString: (ctx, e) => {
if (e.type !== "INPUT") return true;
if (typeof e.data !== "string") return false;
return true;
},
isJSON: (ctx, e) => {
// if (e.type !== "INPUT") return true;
try {
JSON.parse(ctx.inputValue);
return true;
} catch {
return false;
}
},
},
},
);
export default transformMachine;
+11
View File
@@ -0,0 +1,11 @@
import { createApp } from "vue";
import "./index.css";
import router from "./router/index";
import { createPinia } from "pinia";
import App from "./App.vue";
import "highlight.js/styles/stackoverflow-light.css";
const app = createApp(App);
app.use(router)
.use(createPinia())
.mount("#app");
@@ -0,0 +1,19 @@
import { fetchStore } from "@/stores/fetch";
import {
createRouter,
createWebHashHistory,
} from "vue-router";
import routes from "./routes";
const router = createRouter({
history: createWebHashHistory(),
routes,
});
router.beforeEach((to, from, next) => {
fetchStore()
.interruptJob()
.finally(() => {
next();
});
});
export default router;
@@ -0,0 +1,115 @@
import { RouteRecordRaw } from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/",
redirect: "/dashboard",
},
{
path: "/config",
component: () => import("@/views/Config.vue"),
},
{
path: "/console",
component: () => import("@/views/Console.vue"),
},
{
path: "/dashboard",
component: () => import("@/views/DashBoard.vue"),
},
{
path: "/synchronize",
component: () => import("@/views/Synchronize.vue"),
children: [
{
path: "",
redirect: "/synchronize/mbean",
},
{
path: "thread",
component: () => import("@/views/sync/Thread.vue"),
},
// {
// path: "memory",
// component: () => import("@/views/sync/Memory.vue"),
// },
{
path: "jad",
component: () => import("@/views/sync/Jad.vue"),
},
{
path: "retransform",
component: () => import("@/views/sync/Retransform.vue"),
},
{
path: "mbean",
component: () => import("@/views/sync/Mbean.vue"),
},
{
path: "classLoader",
component: () => import("@/views/sync/ClassLoader.vue"),
},
{
path: "heapdump",
component: () => import("@/views/sync/HeapDump.vue"),
},
{
path: "vmtool",
component: () => import("@/views/sync/Vmtool.vue"),
},
{
path: "reset",
component: () => import("@/views/sync/Reset.vue"),
},
{
path: "/synchronize/synchronize",
redirect: "",
},{
path:"ognl",
component:()=>import("@/views/sync/Ognl.vue")
},{
path:"classInfo",
component:()=>import("@/views/sync/ClassInfo.vue")
}
],
},
{
path: "/asynchronize",
component: () => import("@/views/Asynchronize.vue"),
children: [
{
path: "",
redirect: "/asynchronize/stack",
},
{
path: "tt",
component: () => import("@/views/async/Tt.vue"),
},
{
path: "ptofiler",
component: () => import("@/views/async/Profiler.vue"),
},
{
path: "stack",
component: () => import("@/views/async/Stack.vue"),
},
{
path: "monitor",
component: () => import("@/views/async/Monitor.vue"),
},
{
path: "trace",
component: () => import("@/views/async/Trace.vue"),
},
{
path: "watch",
component: () => import("@/views/async/Watch.vue"),
},
{
path: "profiler",
component: ()=>import("@/views/async/Profiler.vue")
}
],
},
];
export default routes;
@@ -0,0 +1,273 @@
import { useInterpret, useMachine } from "@xstate/vue";
import { defineStore } from "pinia";
import { watchEffect } from "vue";
import { publicStore } from "./public";
import { waitFor } from "xstate/lib/waitFor";
import { interpret } from "xstate";
import permachine from "@/machines/perRequestMachine";
// 控制fetch的store
const getEffect = (
M: ReturnType<typeof useMachine>,
fn: (res: ArthasRes) => void,
) =>
watchEffect(() => {
// console.dir(M.state.value.context.response)
if (M.state.value.context.response) {
const response = M.state.value.context.response;
//防止触发额外的副作用,因为输入时context会改变,导致多执行一次effect。。。
M.state.value.context.response = undefined;
fn(response as ArthasRes);
}
});
type Machine = ReturnType<typeof useMachine>;
type MachineService = ReturnType<typeof useInterpret>;
type PollingLoop = {
open(): void;
close(): void;
isOn(): boolean;
invoke(): void;
};
const nullLoop: PollingLoop = {
open() {},
close() {},
isOn() {
return false;
},
invoke() {},
};
export const fetchStore = defineStore("fetch", {
state: () => ({
sessionId: "",
consumerId: "",
requestId: "",
online: false,
wait: false,
/**
* 需要给status计数,不然interupt自动关闭很容易出问题
*/
statusZeroCount: 0,
// 所有用pollingLoop都要
jobRunning: false,
// 对于 pullresults可能会拉同一个结果很多次
jobIdSet: new Set<string>(),
//由于轮询只会轮询一个命令,可以直接挂载当前的轮询机
curPolling: nullLoop,
}),
getters: {
getRequest: (state) =>
(option: ArthasReq) => {
/**
* 对于never,就直接赋值为""
* 对于undefined, 就使用全局默认值
* 对于定义的字符串,则使用定义的值
* @param key
* @returns
*/
const trans = (key: "sessionId" | "requestId" | "consumerId") => {
if (key in option) {
//@ts-ignore
if (option[key] !== undefined) {
//@ts-ignore
return option[key];
} else {
return state[key];
}
}
return "";
};
let sessionId = trans("sessionId");
let requestId = trans("requestId");
let consumerId = trans("consumerId");
const req = new Request("/api", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...option,
sessionId,
consumerId,
requestId,
// 若上面三个属性不传,直接用 as any而不是传undefined
}),
});
return req;
},
},
actions: {
getPollingLoop(
hander: Function,
options: { step?: number; globalIntrupt?: boolean } = {
step: 1000,
globalIntrupt: false,
},
) {
let id = -1;
const { step, globalIntrupt } = options;
const that = this;
const pollingLoop = {
// 自动轮询的可能会被错误打断
open() {
if (!this.isOn()) {
if (globalIntrupt) that.jobRunning = true;
hander();
id = setInterval(
(() => {
if (
//isErr是瞬时的,点击了就会变回去。。。
publicStore().isErr || (!that.jobRunning && globalIntrupt)
) {
this.close();
} else {
hander();
}
}) as TimerHandler,
step,
);
}
},
close() {
if (this.isOn()) {
//用于自动可自动打断,但是要加计时器
if (globalIntrupt) that.jobRunning = false;
clearInterval(id);
id = -1;
}
},
isOn() {
return id !== -1;
},
/**
* 无条件调用传入的hander
*/
invoke() {
hander();
},
};
this.curPolling = pollingLoop;
return pollingLoop;
},
pullResultsLoop(pollingM: Machine,globalIntrupt:boolean=true) {
return this.getPollingLoop(
() => {
pollingM.send({
type: "SUBMIT",
value: {
action: "pull_results",
sessionId: undefined,
consumerId: undefined,
},
});
},
{
globalIntrupt,
},
);
},
onWait() {
if (!this.wait) this.wait = true;
},
waitDone() {
if (this.wait) this.wait = false;
},
getCommonResEffect(M: Machine, fn: (body: CommonRes["body"]) => void) {
return getEffect(M, (res) => {
if (Object.hasOwn(res, "body")) {
fn((res as CommonRes).body);
}
});
},
/**
* 注入enhancer:Proxy<Map<string,string[]>>
*/
getPullResultsEffect(
M: Machine,
fn: (result: ArthasResResult) => void,
) {
return this.getCommonResEffect(M, (body: CommonRes["body"]) => {
if (body.results.length > 0) {
body.results.forEach((result) => {
// 对于既不是状态形状
// console.dir(result)
// if(result.type !=="status") {
// console.log("st",result.jobId)
// if(!this.jobIdSet.has(result.jobId.toString())) {
// 错误已经在machine那里拦截了
fn(result);
// this.jobIdSet.add(result.jobId.toString())
// }
// }
});
}
});
},
interruptJob() {
if (this.jobRunning) {
// 先不管先后端同步的问题
// dashboard可能会寄
this.jobRunning = false;
return this.baseSubmit(interpret(permachine), {
action: "interrupt_job",
sessionId: undefined,
});
}
this.jobRunning = false;
return Promise.reject("There art not job running");
},
openJobRun() {
this.jobRunning = true;
},
isResult(m: MachineService) {
return waitFor(m, (state) => {
return state.hasTag("result");
});
},
tranOgnl(s: string): string[] {
return s.replace(/\r\n\tat/g, "\r\n\t@").split("\r\n\t");
},
/**
* @param fetchM 传入的服务
* @param value 传入的请求
* @returns 待处理的promise
* 貌似不支持这样的类型推断,会出现分布式计算...以后想办法处理
*/
baseSubmit<T extends BindQS>(fetchM: MachineService, value: T["req"]) {
//防止在polling时触发其他命令
if (this.jobRunning) return Promise.reject("there are jobs on running");
fetchM.start();
fetchM.send("INIT");
fetchM.send({
type: "SUBMIT",
value,
});
return this.isResult(fetchM).then(
(state) => {
if (state.matches("success")) {
return Promise.resolve<T["res"]>(state.context.response);
} else {
return Promise.reject("ERROR");
}
},
(err) => {
return Promise.reject(err);
},
);
},
initSession() {
return this.baseSubmit(interpret(permachine), {
action: "init_session",
});
},
asyncInit() {
if (!this.online) {
publicStore().ignore = true;
return this.initSession().then((res) => {
publicStore().ignore = false;
return Promise.resolve(res);
}, (err) => {
publicStore().ignore = false;
return Promise.reject(err);
});
}
return Promise.resolve("alrealy init");
},
},
});
@@ -0,0 +1,101 @@
import { defineStore } from "pinia";
import { Ref, ref, watch, watchEffect } from "vue";
import { useMachine } from "@xstate/vue";
const getEffect = (
M: ReturnType<typeof useMachine>,
fn: (res: ArthasRes) => void,
) =>
watchEffect(() => {
if (M.state.value.context.response) {
const response = M.state.value.context.response;
fn(response as ArthasRes);
}
});
export const publicStore = defineStore("public", { // Public项目唯一id
state: () => ({
userMsg: {},
// 当初设计不好,应该写成setter,loop的意外错误关闭得挂到errdialog了。。。
isErr: false,
/**
* isInput 是对input组件的锁,要使用inputVal,调用inputval的组件还要自定义一个锁
*/
isInput: false,
inputVal: "",
ErrMessage: "bug!!!",
isSuccess: false,
SuccessMessage: "bug!!!",
isWarn: false,
warnMessage: "",
warningFn: () => {},
/**
* 忽略弹窗
*/
ignore: false,
}),
getters: {
getUserMsg: (state) => {
return state.userMsg;
},
},
actions: {
getCommonResEffect: (
M: ReturnType<typeof useMachine>,
fn: (body: CommonRes["body"]) => void,
) => {
return getEffect(M, (res) => {
if (Object.hasOwn(res, "body")) {
fn((res as CommonRes).body);
}
});
},
interruptJob(M: ReturnType<typeof useMachine>) {
M.send({
type: "SUBMIT",
value: {
action: "interrupt_job",
} as AsyncReq,
});
},
/**
*
* @param inputRef 组件提供的值的存储区
* @param getVal 从inputRef到全局缓冲区的处理流程
* @param setVal 冲缓冲区到inputRef的处理流程
* @returns 独占缓冲区函数
*/
inputDialogFactory<T = string>(
inputRef: Ref<T>,
getVal: (raw: string) => T,
setVal: (input: Ref<T>) => string,
) {
// 初始化一个响应式的锁
let mutex = ref(false);
// 发布订阅模式,把函数的闭包里的mutex和缓冲区的锁注册到vue上
watchEffect(() => {
if (mutex.value && !this.isInput) {
//先上锁,防止再次触发该副作用
mutex.value = false;
// 把缓冲区的值输入到需要使用的组件里
inputRef.value = getVal(this.inputVal);
// 清空缓冲区
this.inputVal = "";
}
});
return () => {
this.$patch({
// 打开缓冲区
isInput: true,
// 把当前值导入到缓冲区
inputVal: setVal(inputRef),
});
// 解锁
mutex.value = true;
};
},
nanoToMillis(nanoSeconds: number): number {
return nanoSeconds / 1000000;
},
},
});
@@ -0,0 +1,9 @@
import { defineStore } from "pinia";
export const transfromStore = defineStore("transformStore", {
actions: {
transformStackTrace(trace: StackTrace) {
return `${trace.className}.${trace.methodName} (${trace.fileName}: ${trace.lineNumber})`;
},
},
});
@@ -0,0 +1,35 @@
<script setup lang="ts">
import SelectCmd from '@/components/routeTo/SelectCmd.vue';
const routes: { cmd: string, url: string }[] = [
{ cmd: 'tt', url: 'tt' },
{
cmd: 'stack', url: "stack"
}, {
cmd: 'monitor',
url: 'monitor'
}, {
cmd: 'trace',
url: 'trace'
}, {
cmd: 'watch',
url: 'watch'
}, {
cmd: 'profiler',
url: 'profiler'
},
]
</script>
<template>
<div class="flex">
<SelectCmd :routes="routes"></SelectCmd>
<div class="p-2 h-[90vh] overflow-auto flex-1 pointer-events-auto">
<RouterView></RouterView>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,179 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { useInterpret, useMachine } from '@xstate/vue';
import { onBeforeMount, reactive, ref, watchEffect } from 'vue';
import OptionConfigMenu from '@/components/show/OptionConfigMenu.vue';
import SwitchInput from '@/components/input/SwitchInput.vue';
import { publicStore } from '@/stores/public';
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue"
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { fetchStore } from '@/stores/fetch';
import permachine from '@/machines/perRequestMachine';
const fetchS = fetchStore()
const sysEnvMap = reactive(new Map<string, string[]>())
const sysPropMap = reactive(new Map<string, string[]>())
const perfcounterMap = reactive(new Map<string, string[]>())
const vmOptionM = useMachine(machine)
const pwd = ref("?")
const vmOptionMTree = reactive([] as VmOption[])
// 初始化
onBeforeMount(() => {
fetchS.baseSubmit(useInterpret(permachine), {
action: "exec",
command: "pwd"
}).then(
res => {
const result = (res as CommonRes).body.results[0]
if (result.type == "pwd") {
pwd.value = result.workingDir
}
}
)
vmOptionM.send("INIT")
vmOptionM.send({
type: "SUBMIT",
value: {
action: "exec",
command: "vmoption"
}
})
})
// 处理展示的树形数据
const handleTree = (data: Record<string, string | boolean | number>, map: Map<string, string[]>): string[] => {
map.clear()
let res: string[] = []
Object.entries(data).forEach(([k, v]) => {
res.push(k)
if (typeof v === "boolean") v = v.toString()
if (typeof v === "number") v = v.toString()
if (k.toLowerCase().includes("path")) {
map.set(k, v.split(":").filter(v => v.trim() !== ''))
} else if (v.includes(";")) {
map.set(k, v.split(";").filter(v => v.trim() !== ''))
} else {
map.set(k, [v])
}
})
return res
}
const handleEnvTree = (data: Record<string, string>) => handleTree(data, sysEnvMap)
const handlePropTree = (data: Record<string, string>) => handleTree(data, sysPropMap)
// 处理可修改参数的树形结构
// const handleOptionTree = (data:Record<string>)
watchEffect(() => {
const response = vmOptionM.state.value.context.response
if (response) {
if (Object.hasOwn(response, "body")) {
console.log(response, "vmoption")
const result = (response as CommonRes).body.results[0]
if (result.type == "vmoption") {
// handlePropTree(result.props).forEach(v => {
// sysPropTree.push(v)
// })
// 先clear一下之前的东西
vmOptionMTree.length = 0
console.log(result.vmOptions, "watchEffect!!!")
result.vmOptions.forEach(v => {
vmOptionMTree.push(v)
})
}
}
}
})
const vmOptionSend = (pre: string) => (v: { key: string, value: boolean | string }) => {
if (pre === "HeapDumpPath" && v.value === "") {
publicStore().$patch({ ErrMessage: "HeapDumpPath can't be set \"\" ", isErr: true })
return
} vmOptionM.send({
type: 'SUBMIT', value: {
action: "exec",
command: `vmoption ${pre} ${v.value === "" ? '\"\"' : v.value}`
}
})
}
const jvmMap = reactive(new Map<string, string[]>())
const getJvm = () => fetchS.baseSubmit(useInterpret(permachine), {
action: "exec",
command: "jvm"
}).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "jvm") {
jvmMap.clear()
Object.entries(result.jvmInfo).forEach(([k, v]) => {
jvmMap.set(k, v.map(v => `${v.name} : ${v.value}`))
})
}
})
const getSysenv = () => fetchS.baseSubmit(useInterpret(permachine), {
action: "exec",
command: "sysenv",
}).then(res => {
let result = (res as CommonRes).body.results[0]
if (result.type == "sysenv") {
handleEnvTree(result.env)
}
})
const getSysprop = () => fetchS.baseSubmit(useInterpret(permachine), {
action: "exec",
command: "sysprop",
}).then(res => {
let result = (res as CommonRes).body.results[0]
if (result.type == "sysprop") {
handlePropTree(result.props)
}
})
const getPerCounter = () => fetchS.baseSubmit(useInterpret(permachine), { action: "exec", command: "perfcounter -d" }).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "perfcounter") {
const perfcounters = result.perfCounters
perfcounterMap.clear()
perfcounters.forEach(v => {
perfcounterMap.set(v.name, Object.entries(v).filter(v => v[0] !== "name").map(([key, value]) => `${key} : ${value}`))
})
}
})
</script>
<template>
<div class="p-2 h-[90vh] overflow-y-scroll">
<article>
<div class="flex items-center">
<div class="btn-info btn my-2 btn-sm normal-case">workingDir</div>
<div class="bg-base-200 w-full text-base-content pl-2"> {{ pwd }}</div>
</div>
<CmdResMenu title="sysenv" :map="sysEnvMap" @click="getSysenv" />
<CmdResMenu title="sysprop" :map="sysPropMap" @click="getSysprop" />
<option-config-menu title="vmOption" :list="vmOptionMTree" title-key-name="name">
<template #item="{ kv, itemTitle, idx }">
<switch-input :send="vmOptionSend(itemTitle)" :data="{ key: kv[0], value: kv[1] }" v-if="kv[0] === 'value'"
:class="{ 'border-t-4': (idx > 0), 'border-base-100': (idx > 0) }">
</switch-input>
<div v-else class="flex " :class="{ 'border-t-4': (idx > 0), 'border-base-100': (idx > 0) }">
<div class="bg-blue-200 w-1/5 p-1">{{ kv[0] }}</div>
<div class="grid place-items-center w-3/5">{{ kv[1] }}</div>
</div>
</template>
</option-config-menu>
<CmdResMenu title="jvm" :map="jvmMap" class="w-full" @click="getJvm" />
<CmdResMenu title="perfcounter" :map="perfcounterMap" @click="getPerCounter" />
</article>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue';
import { useMachine } from '@xstate/vue';
import machine from '@/machines/consoleMachine';
const fetchM = useMachine(machine)
const val = ref(JSON.stringify({
action: "exec",
command: "version"
}));
onBeforeMount(()=>{
fetchM.send("INIT")
})
const submitCommand = ()=>{
console.log('别报错了')
fetchM.send({ type: 'SUBMIT', value: val.value})
}
</script>
<template>
<div class="flex flex-col p-2">
<form class="h-[10vh] flex items-center border shadow" @submit.prevent="submitCommand">
<label for="command-input" class=" m-2 ">command:</label>
<div class=" flex-auto grid place-items-start">
<input type="text" placeholder="input command" v-model="val" id="command-input"
class=" outline-1 focus-visible:outline-gray-600 border rounded hover:shadow h-10 transition w-11/12 box-border">
</div>
<button class="hover:shadow w-24 h-10 border rounded-md mr-20 "
>
submit
</button>
</form>
<article class="flex-1 bg-white overflow-auto max-h-[70vh]">
<section v-for="(v, i) in fetchM.state.value.context.resArr" :key="i"
class="w-full rounded-sm mb-2 p-2 bg-green-200 box-border break-all"
:class="{ 'bg-blue-200': v&&!Object.hasOwn(v, 'jobId')}"
>
{{ JSON.stringify(v) }}
</section>
</article>
</div>
</template>
<style scoped>
</style>·
@@ -0,0 +1,497 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import { useInterpret, useMachine } from '@xstate/vue';
import { onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import * as echarts from 'echarts/core';
import {
TooltipComponent,
TooltipComponentOption,
LegendComponent,
LegendComponentOption,
DatasetComponentOption,
GridComponentOption,
ToolboxComponentOption,
GridComponent,
ToolboxComponent
} from 'echarts/components';
import {
BarChart,
BarSeriesOption,
LineChart,
LineSeriesOption,
PieChart,
PieSeriesOption
} from 'echarts/charts';
import {
LabelLayout, UniversalTransition
} from 'echarts/features';
import {
SVGRenderer
} from 'echarts/renderers';
import { dispose, ECharts } from 'echarts/core';
import permachine from '@/machines/perRequestMachine';
import { onBeforeRouteLeave } from 'vue-router';
type EChartsOption = echarts.ComposeOption<
DatasetComponentOption | PieSeriesOption
>
type GcEChartsOption = echarts.ComposeOption<
ToolboxComponentOption | TooltipComponentOption | GridComponentOption | LegendComponentOption | BarSeriesOption | LineSeriesOption
>
const fetchS = fetchStore()
const { getCommonResEffect } = publicStore()
const dashboadM = useInterpret(permachine)
const dashboadResM = useMachine(machine)
const loop = fetchS.pullResultsLoop(dashboadResM)
const toMb = (b: number) => Math.floor(b / 1024 / 1024)
const gcInfos = reactive(new Map<string, string[]>())
const memoryInfo = reactive(new Map<string, string[]>())
const threads = reactive(new Map<string, string[]>())
const runtimeInfo = reactive(new Map<keyof RuntimeInfo, string>())
const pri = ref(3)
const publiC = publicStore()
const tableResults = reactive([] as Map<string, string>[])
const keyList: (keyof ThreadStats)[] = [
"id",
"name",
"cpu",
"daemon",
"deltaTime",
"group",
"interrupted",
"priority",
"state",
"time",
]
let dashboardId = -1
let heapChart: ECharts
let nonheapChart: ECharts
let bufferPoolChart: ECharts
let gcChart: ECharts
const clearChart = (...charts: ECharts[]) => {
charts.forEach(chart => {
if (chart !== null && chart !== undefined) chart.dispose()
})
}
const transformMemory = (result: ArthasResResult) => {
if (result.type === "dashboard") {
const heaparr: { value: number, name: string }[] = [
]
result.memoryInfo.heap.filter(v => v.name !== "heap").forEach(v => {
const arr: string[] = []
arr.push('max : ' + toMb(v.max))
arr.push('total : ' + toMb(v.total))
arr.push('used : ' + toMb(v.used))
const usage: number = (v.max > 0 ? (v.used / v.max) : (v.used / v.total)) * 100
heaparr.push({ value: toMb(v.used), name: `${v.name}(${usage.toFixed(2)}%)` })
arr.push(usage + '%')
memoryInfo.set(v.name, arr)
})
heaparr.push({
value: Math.floor((result.memoryInfo.heap[0].max > 0 ? (result.memoryInfo.heap[0].max - result.memoryInfo.heap[0].used) : (result.memoryInfo.heap[0].total - result.memoryInfo.heap[0].used)) / 1024 / 1024),
name: "free",
})
heapChart && heapChart.setOption({
series: {
data: heaparr
}
} as EChartsOption)
const nonheaparr: {
value: number, name: string,
}[] = []
result.memoryInfo.nonheap.filter(v => v.name !== "nonheap").forEach(v => {
const arr: string[] = []
arr.push('max : ' + toMb(v.max))
arr.push('total : ' + toMb(v.total))
arr.push('used : ' + toMb(v.used))
const usage: number = (v.used / v.total) * 100
nonheaparr.push({ value: toMb(v.used), name: `${v.name}(${usage.toFixed(2)}%)` })
arr.push(usage * 100 + '%')
memoryInfo.set(v.name, arr)
})
nonheapChart && nonheapChart.setOption({ series: { data: nonheaparr } } as EChartsOption)
const bufferPoolarr: {
value: number, name: string,
}[] = []
result.memoryInfo.buffer_pool.filter(v => v.name !== "buffer_pool;").forEach(v => {
bufferPoolarr.push({ value: toMb(v.used), name: `${v.name}` })
})
bufferPoolChart && bufferPoolChart.setOption({ series: { data: bufferPoolarr } } as EChartsOption)
}
}
const transformThread = (result: ArthasResResult, end: number) => {
if (result.type !== "dashboard") return;
// result.threads.filter((v, i) => i < pri.value).forEach(thread => {
// // threads.set(v.name, Object.entries(v).filter(([k, v]) => k !== "name").map(([k, v]) => `${k} : ${v}`))
// const map = new Map()
// Object.entries(thread).map(([k, v]) => map.set(k, v.toString().trim() || "-"))
// tableResults.unshift(map)
// })
for (let i = 0; i < end && i < result.threads.length; i++) {
const thread = result.threads[i]
const map = new Map()
Object.entries(thread).forEach(([k, v]) => map.set(k, v.toString().trim() || "-"))
tableResults.unshift(map)
}
}
const transformGc = (result: ArthasResResult) => {
if (result.type !== "dashboard") return;
const gcCountData: number[] = []
const gcTimeData: number[] = []
const gcxdata: string[] = []
result.gcInfos.forEach(v => {
// gcInfos.set(v.name, [v.collectionCount.toString(), v.collectionTime.toString()])
gcxdata.push(v.name)
gcCountData.push(v.collectionCount)
gcTimeData.push(v.collectionTime)
})
gcChart.setOption({
xAxis: {
type: 'category',
axisTick: {
alignWithLabel: true
},
// prettier-ignore
data: gcxdata
}, series: [{
name: "collectionCount",
type: 'bar',
data: gcCountData
}, {
name: "collectionTime",
type: 'bar',
data: gcTimeData
}]
} as GcEChartsOption)
}
const setPri = publiC.inputDialogFactory(
pri,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 3 : valRaw
},
(input) => input.value.toString(),
)
const transformRuntimeInfo = (result: ArthasResResult) => {
if (result.type !== "dashboard") return;
for (const key in result.runtimeInfo as RuntimeInfo) {
runtimeInfo.set(key as keyof RuntimeInfo, result.runtimeInfo[key as keyof RuntimeInfo].toString())
}
}
getCommonResEffect(dashboadResM, body => {
if (body.results.length > 0 && dashboardId >= 0) {
const result = body.results.find(v => v.type === "dashboard" && v.jobId === dashboardId)
if (result && result.type === "dashboard") {
memoryInfo.clear()
transformMemory(result)
runtimeInfo.clear()
transformRuntimeInfo(result)
threads.clear()
tableResults.length = 0
transformThread(result, pri.value)
gcInfos.clear()
transformGc(result)
}
}
})
// 处理初始化请求
onBeforeMount(async () => {
dashboadResM.send("INIT")
fetchS
.asyncInit()
.finally(
() => {
fetchS.baseSubmit(dashboadM, {
action: "async_exec",
command: "dashboard",
sessionId: undefined
}).then(
res => {
dashboardId = (res as AsyncRes).body.jobId
loop.open()
}
)
}
)
})
// 处理dom
onMounted(() => {
// init
const clearDom = (...doms: HTMLElement[]) => {
doms.forEach(dom => {
dispose(dom)
})
}
clearChart(nonheapChart, heapChart, bufferPoolChart, gcChart)
const heapDom = document.getElementById('heapMemory')!
const nonheapDom = document.getElementById('nonheapMemory')!
const bufferPoolDom = document.getElementById('bufferPoolMemory')!
const gcDom = document.getElementById('gc-info')!
clearDom(heapDom, nonheapDom, bufferPoolDom, gcDom)
echarts.use(
[TooltipComponent, LegendComponent, PieChart, SVGRenderer, LabelLayout, ToolboxComponent, GridComponent, BarChart, LineChart, UniversalTransition]
);
const heapoption: EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}:{c}M {d}'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'heap memory',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: true,
label: {
show: false,
position: 'center',
},
labelLine: {
show: false
},
data: [
]
}
]
};
const nonheapoption: EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}:{c}M'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'nonheap memory',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
formatter: '{b}:{c}M'
},
labelLine: {
show: false
},
data: [
]
}
]
};
const bufferPooloption: EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{c}M'
},
legend: {
top: '5%',
left: 'center'
},
series: [
{
name: 'buffer_pool memory',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: true,
label: {
show: false,
position: 'outside',
},
labelLine: {
show: false
},
data: [
{
value: 0,
name: '',
}
]
}
]
};
const colors = ['#5470C6', '#91CC75'];
const gcoption: GcEChartsOption = {
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
grid: {
right: '20%'
},
legend: {
data: ['collectionCount', 'collectionTime']
},
xAxis: [
{
type: 'category',
axisTick: {
alignWithLabel: true
},
// prettier-ignore
data: []
}
],
toolbox: {
feature: {
dataView: { show: true, readOnly: true },
}
},
yAxis: [
{
type: 'value',
name: 'collectionCount',
position: 'left',
alignTicks: true,
axisLine: {
show: true,
lineStyle: {
color: colors[0]
}
},
axisLabel: {
formatter: '{value}'
}
},
{
type: 'value',
name: 'collectionTime',
position: 'right',
alignTicks: true,
axisLine: {
show: true,
lineStyle: {
color: colors[1]
}
},
axisLabel: {
formatter: '{value} ms'
}
},
],
series: [
{
name: 'collectionCount',
type: 'bar',
data: [
]
},
{
name: 'collectionTime',
type: 'bar',
yAxisIndex: 1,
data: [
]
},
]
};
heapChart = echarts.init(heapDom);
heapoption && heapChart.setOption(heapoption);
nonheapChart = echarts.init(nonheapDom);
nonheapoption && nonheapChart.setOption(nonheapoption);
bufferPoolChart = echarts.init(bufferPoolDom);
bufferPooloption && bufferPoolChart.setOption(bufferPooloption);
// gcInfosChart
gcChart = echarts.init(gcDom);
gcoption && gcChart.setOption(gcoption);
})
onBeforeUnmount(async () => {
loop.close()
clearChart(nonheapChart, heapChart, bufferPoolChart, gcChart)
})
</script>
<template>
<div class="p-2 pointer-events-auto flex flex-col h-full">
<div class="input-btn-style mb-4 h-32 flex flex-wrap flex-col items-start overflow-auto min-h-[6rem]">
<div v-for="(cv, ci) in runtimeInfo" :key="ci" class="flex mb-1 pr-2">
<span class="bg-primary-focus text-primary-content border border-primary-focus w-44 px-2 rounded-l">
{{ cv[0] }}
</span>
<span class="bg-base-200 border border-primary-focus rounded-r px-2 flex-1">
{{cv[1]}}
</span>
</div>
</div>
<!-- <CmdResMenu title="threads" :map="threads" class="w-full flex justify-center" /> -->
<div class="flex justify-evenly mb-4 flex-1 h-80">
<div id="heapMemory" class="w-80 h-80 flex-1 input-btn-style mr-4"></div>
<div id="nonheapMemory" class="w-80 h-80 flex-1 input-btn-style mr-4"></div>
<div id="bufferPoolMemory" class="w-80 h-80 flex-1 input-btn-style"></div>
</div>
<div class="w-full flex justify-start items-start flex-1">
<div id="gc-info" class="w-[40rem] h-80 input-btn-style p-2 mr-4"></div>
<div class="input-btn-style overflow-auto flex-1 h-80">
<div class="flex justify-end mb-2">
<button class="btn btn-sm btn-outline" @click="setPri">limit:{{pri}}</button>
</div>
<div class="overflow-x-auto">
<table class="table table-compact w-full group">
<thead>
<tr>
<th class="border-slate-300" v-for="(v,i) in keyList" :key="i" :class="{'group-first:z-0':i==0}">{{v}}</th>
</tr>
</thead>
<tbody class="">
<tr v-for="(map, i) in tableResults" :key="i">
<td class="border border-slate-300" v-for="(key,j) in keyList" :key="j">
{{map.get(key)}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import SelectCmd from '@/components/routeTo/SelectCmd.vue';
const routes: { cmd: string, url: string }[] = [
{
cmd: "thread",
url: "thread"
}, {
cmd: "classInfo",
url: "classInfo"
// }, {
// cmd: "perfcounter",
// url: "perfcounter"
}, {
cmd: "classLoader",
url: "classLoader"
}, {
cmd: "jad",
url: "jad"
}, {
cmd: "retransform",
url: "retransform"
}, {
cmd: "mbean",
url: "mbean"
}, {
cmd: "heapdump",
url: "heapdump"
}, {
cmd: "vmtool",
url: "vmtool"
}, {
cmd: "reset",
url: "reset"
},{
cmd: "ognl",
url: "ognl"
}
]
</script>
<template>
<div class="h-[90vh] overflow-auto flex">
<SelectCmd :routes="routes"></SelectCmd>
<div class="p-2 overflow-y-scroll w-full flex flex-col flex-1 pointer-events-auto">
<RouterView></RouterView>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,348 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { useInterpret, useMachine } from '@xstate/vue';
import {
ListboxOption,
Listbox,
ListboxButton,
ListboxOptions
} from "@headlessui/vue"
import MethodInput from '@/components/input/MethodInput.vue';
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import { onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import Enhancer from '@/components/show/Enhancer.vue';
import { publicStore } from '@/stores/public';
import * as echarts from 'echarts/core';
import {
TitleComponent,
TitleComponentOption,
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
DataZoomComponent,
DataZoomComponentOption
} from 'echarts/components';
import {
BarChart,
BarSeriesOption,
LineChart,
LineSeriesOption
} from 'echarts/charts';
import {
UniversalTransition
} from 'echarts/features';
import {
SVGRenderer
} from 'echarts/renderers';
import { ECharts, number } from 'echarts/core';
echarts.use(
[TitleComponent, ToolboxComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, BarChart, LineChart, SVGRenderer, UniversalTransition]
);
type EChartsOption = echarts.ComposeOption<
TitleComponentOption | ToolboxComponentOption | TooltipComponentOption | GridComponentOption | LegendComponentOption | DataZoomComponentOption | BarSeriesOption | LineSeriesOption
>
const pollingM = useMachine(machine)
const fetchS = fetchStore()
const { pullResultsLoop, getCommonResEffect } = fetchS
const fetchM = useInterpret(permachine)
const loop = pullResultsLoop(pollingM)
const enhancer = ref(undefined as undefined | EnchanceResult)
const cycleV = ref(120)
const publicS = publicStore()
type KMD = | keyof MonitorData
// const keyList: string[] = [
// "className",
// "methodName",
// "cost",
// "success",
// "failed",
// "fail-rate",
// "total",
// ]
const modelist: { name: string, value: string }[] = [
{ name: "before", value: "-b" },
{ name: "finish", value: "" }
]
const mode = ref(modelist[1])
// const tableResults = reactive([] as Map<string, string[] | string>[])
const chartContext: {
count: number,
myChart?: ECharts,
costChart?: ECharts,
categories: number[],
data: number[],
cur: number,
max: number,
successData: number[],
failureData: number[]
} = {
max: 0,
cur: 0,
count: 40,
myChart: undefined,
costChart: undefined,
categories: [],
data: [],
successData: [],
failureData: [],
}
for (let i = 0; i < chartContext.count; i++) { chartContext.categories[i] = i + 1 }
const chartOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#283b56'
}
}
},
legend: {},
toolbox: {
show: true,
feature: {
dataView: { readOnly: false },
}
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: chartContext.categories
}
],
yAxis: [{
type: 'value',
name: 'count'
}
],
series: [
{
name: 'success',
type: 'bar',
stack: 'count',
data: [],
// itemStyle: {
// color: "#9836cd"
// }
},
{
name: 'failure',
type: 'bar',
stack: "count",
data: [],
itemStyle: {
color: "#ff0000",
}
},
]
};
const costOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#283b56'
}
}
},
legend: {},
toolbox: {
show: true,
feature: {
dataView: { readOnly: false },
}
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: chartContext.categories
}
],
yAxis: [
{
type: 'value',
scale: true,
name: 'cost(ms)',
min: 0,
boundaryGap: [0.2, 0.2]
}
],
series: [
{
name: 'cost',
type: 'bar',
data: chartContext.data
}
]
}
const updateChart = (data: MonitorData) => {
while (chartContext.cur > chartContext.count) {
chartContext.data.shift()
chartContext.successData.shift()
chartContext.failureData.shift()
chartContext.cur--
}
chartContext.data.push(data.cost)
chartContext.failureData.push(data.failed)
chartContext.successData.push(data.success)
chartContext.cur++
chartContext.myChart!.setOption<EChartsOption>({
xAxis: [
{
data: chartContext.categories
}
],
series: [{
data: chartContext.successData
}, {
data: chartContext.failureData
}
]
})
chartContext.costChart!.setOption<EChartsOption>({
xAxis: [
{
data: chartContext.categories
}
],
series: [
{
data: chartContext.data
}
]
})
}
const resetChart = () => {
chartContext.data.length = 0
chartContext.failureData.length = 0
chartContext.successData.length = 0
chartContext.myChart!.setOption<EChartsOption>({
xAxis: [
{
data: chartContext.categories
}
],
series: [{
data: chartContext.successData
}, {
data: chartContext.failureData
}
]
})
chartContext.costChart!.setOption<EChartsOption>({
xAxis: [
{
data: chartContext.categories
}
],
series: [
{
data: chartContext.data
}
]
})
}
const transform = (result: ArthasResResult) => {
if (result.type === "monitor") {
result.monitorDataList.forEach(data => {
updateChart(data)
})
}
if (result.type === "enhancer") {
enhancer.value = result
}
}
getCommonResEffect(pollingM, body => {
if (body.results.length > 0) {
body.results.forEach(result => {
transform(result)
})
}
})
const changeCycle = publicS.inputDialogFactory(
cycleV,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 120 : valRaw
},
(input) => input.value.toString()
)
onBeforeMount(() => {
fetchS.asyncInit()
pollingM.send("INIT")
})
onMounted(() => {
const chartDom = document.getElementById('monitorchart')!;
chartContext.myChart = echarts.init(chartDom);
chartOption && chartContext.myChart.setOption(chartOption)
chartContext.costChart = echarts.init(document.getElementById('monitorchartcost')!);
chartOption && chartContext.costChart.setOption(costOption)
})
onBeforeUnmount(() => {
loop.close()
})
const submit = async (data: { classItem: Item, methodItem: Item, conditon: string }) => {
enhancer.value = undefined
// tableResults.length = 0
let condition = data.conditon.trim() == "" ? "" : `'${data.conditon.trim()}'`
let cycle = `-c ${cycleV.value}`
fetchS.baseSubmit(fetchM, {
action: "async_exec",
command: `monitor -c 5 ${data.classItem.value} ${data.methodItem.value} ${condition}`,
sessionId: undefined
}).then(
res => loop.open()
)
}
</script>
<template>
<MethodInput :submit-f="submit" class="mb-4" ncondition>
<template #others>
<Listbox v-model="mode">
<div class=" relative mx-2 ">
<ListboxButton class="btn btn-sm btn-outline w-40">{{ mode.name }}</ListboxButton>
<ListboxOptions
class="absolute w-40 mt-2 border overflow-hidden rounded-md hover:shadow-xl transition bg-white z-10">
<ListboxOption v-for="(am,i) in modelist" :key="i" :value="am" v-slot="{active, selected}">
<div class=" p-2 transition" :class="{
'bg-neutral text-neutral-content': active,
'bg-neutral-focus text-neutral-content': selected,
}">
{{ am.name }}
</div>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
<button class="btn btn-sm btn-outline" @click="changeCycle">cycle time:{{cycleV}}</button>
</template>
</MethodInput>
<Enhancer :result="enhancer" v-if="enhancer" class="mb-4"></Enhancer>
<div id="monitorchart" class="input-btn-style h-60 w-full pointer-events-auto transition mb-2"></div>
<div id="monitorchartcost" class="input-btn-style h-60 w-full pointer-events-auto transition"></div>
</template>
<style>
.bg {
background: #9836cd;
}
</style>
@@ -0,0 +1,250 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { useInterpret } from '@xstate/vue';
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, Ref, ref, watchEffect } from 'vue';
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@headlessui/vue"
import { publicStore } from '@/stores/public';
import TodoList from '@/components/input/TodoList.vue';
import { fetchStore } from '@/stores/fetch';
import { interpret } from 'xstate';
const fetchM = useInterpret(permachine)
const publicS = publicStore()
const fetchS = fetchStore()
let eventList = reactive([] as string[]);
let selectEvent = ref("cpu")
let includesVal = reactive(new Set<string>())
let excludesVal = reactive(new Set<string>())
let framebuf = ref(1000000)
let duration = ref(300)
let profilerStatus = ref({
is: false,
message: ""
})
let outputPath = ref("")
let samples = ref(0)
const support = ref(false)
let fileformat = ref("%t-%p.html")
const getStatusLoop = fetchS.getPollingLoop(() => {
const statusM = interpret(permachine)
fetchS.baseSubmit(statusM, {
command: "profiler status",
action: "exec",
sessionId: "",
}).then(
res => {
support.value = true
if (res) {
let result = (res as CommonRes).body.results[0]
if (result.type == "profiler") {
if (result.executeResult.search("not") >= 0) {
profilerStatus.value.is = false
} else profilerStatus.value.is = true
profilerStatus.value.message = result.executeResult
}
}
},
reject => {
getStatusLoop.close()
}
)
}, {
step: 2000,
})
const getSampleLoop = fetchS.getPollingLoop(() => {
let statusM = interpret(permachine)
fetchS.baseSubmit(statusM, {
command: "profiler getSamples",
action: "exec",
// 置空sessionId,使得不与session冲突
sessionId: ""
}).then(
res => {
if (res) {
let result = (res as CommonRes).body.results[0]
if (result.type == "profiler") {
samples.value = parseInt(result.executeResult)
}
}
}
)
}, {
step: 2000,
})
const changeFramebuf = publicS.inputDialogFactory(
framebuf,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 1000000 : valRaw
},
(input) => input.value.toString()
)
const changeFile = publicS.inputDialogFactory(fileformat,
(raw) => raw.trim(),
(input) => input.value)
const changeDuration = publicS.inputDialogFactory(
duration,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 300 : valRaw
},
(input) => input.value.toString()
)
const restartInit = () => {
profilerStatus.value.is = true
outputPath.value = ""
}
const transformStartProps = () => {
let start = "start"
let evenOption = ""
let includeOption = ""
let excludeOption = ""
let file = "--file arthas-output/"
if (selectEvent.value !== "all") {
evenOption = "--event " + selectEvent.value
}
for (const v of includesVal) {
includeOption += "--include " + v + " "
}
for (const v of excludesVal) {
excludeOption += "--exclude " + v + " "
}
file += fileformat
return {
start,
evenOption,
includeOption,
excludeOption,
file
}
}
const startSubmit = () => {
const { start, evenOption, includeOption, excludeOption, file } = transformStartProps()
fetchS.baseSubmit(fetchM, {
action: "exec",
command: `profiler ${start} ${evenOption} ${includeOption} ${excludeOption} ${fileformat}`,
sessionId: undefined
})
.then(restartInit)
}
const stopProfiler = () => fetchS.baseSubmit(fetchM, {
action: "exec",
command: "profiler stop"
}).then(
res => {
profilerStatus.value.is = false
let result = (res as CommonRes).body.results[0]
if (result.type === "profiler" && result.outputFile) {
outputPath.value = result.outputFile
let reg = /arthas-output\/.*/
let arr = reg.exec(result.outputFile)
if (arr && arr.length > 0) {
let url = window.origin + "/" + arr[0]
window.open(url)
}
}
}
)
const resumeProfiler = () => fetchS.baseSubmit(fetchM, {
action: "exec",
command: "profiler resume"
}).then(
restartInit
)
const toOutputDir = () => window.open(window.location.origin + "/arthas-output/")
onBeforeMount(async () => {
publicS.inputVal = ""
includesVal.clear()
excludesVal.clear()
getStatusLoop.open()
getSampleLoop.open()
fetchS.asyncInit()
await fetchS.baseSubmit(fetchM, {
action: "exec",
command: "profiler list"
}).then(
res => {
let result = (res as CommonRes).body.results[0]
if (result.type == "profiler") {
result.executeResult.split('\n').forEach(raw => {
let cmd = raw.trim();
if (!["Basic events:",
"Java method calls:",
"Perf events:",
""
].includes(cmd)) eventList.push(cmd)
})
}
}
)
}
)
onBeforeUnmount(() => {
getStatusLoop.close()
getSampleLoop.close()
})
</script>
<template>
<template v-if="!support">
<div class="flex py-2 border-b-2 border-gray-300">
<h3 class="text-lg w-40">status: </h3>
<div class="mx-2">
<div>{{profilerStatus.message}}</div>
<div v-if="profilerStatus.is">{{samples}} samples</div>
</div>
</div>
<div class="flex border-b-2 border-gray-300 items-center py-2" v-if="!profilerStatus.is">
<h3 class="text-lg w-40">How to start: </h3>
<Listbox v-model="selectEvent">
<div class=" relative mx-2">
<ListboxButton class="btn btn-sm btn-outline w-52 "> even:
{{ selectEvent}}
</ListboxButton>
<ListboxOptions
class=" absolute w-52 mt-2 border py-2 rounded-md hover:shadow-xl transition bg-white max-h-80 overflow-y-auto">
<ListboxOption v-for="(e,i) in eventList" :key="i" :value="e" v-slot="{active, selected}">
<div class=" p-2 transition break-words text-base-100" :class="{
'bg-neutral text-neutral-content': active,
'bg-neutral-focus text-neutral-content': selected,
}">
{{ e }}
</div>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
<button class="btn btn-sm btn-outline mr-2" @click="changeDuration">duration :{{duration}}</button>
<button class="btn btn-sm btn-outline mr-2" @click="changeFramebuf">framebuf :{{framebuf}}</button>
<button class="btn btn-sm btn-outline mr-2" @click="changeFile">file :<span class="normal-case">{{fileformat}}</span></button>
<TodoList title="include" :val-set="includesVal" class=" mr-2"></TodoList>
<TodoList title="exclude" :val-set="excludesVal" class="mr-2"></TodoList>
<button class="btn btn-primary btn-sm btn-outline" @click="startSubmit">start</button>
</div>
<div class="flex items-center border-b-2 border-gray-300 py-2">
<h3 class="text-lg w-40">Resume or stop: </h3>
<button class="btn btn-primary btn-sm btn-outline mx-2" @click="resumeProfiler" v-if="!profilerStatus.is">resume</button>
<button class="btn btn-primary btn-sm btn-outline" @click="stopProfiler" v-if="profilerStatus.is">stop</button>
</div>
<div class="flex items-center py-2">
<h3 class="text-lg w-40">output file path: </h3>
<div class=" ml-2" v-if="outputPath.trim() !== ''">{{ outputPath }}</div>
<button class="btn btn-primary btn-sm btn-outline ml-2" @click="toOutputDir">go to the output direction</button>
</div>
</template>
<!-- <div v-else>
Your system is not supported!
</div> -->
</template>
@@ -0,0 +1,120 @@
<script setup lang="ts">
import MethodInput from '@/components/input/MethodInput.vue';
import machine from '@/machines/consoleMachine';
import permachine from '@/machines/perRequestMachine';
import { fetchStore } from '@/stores/fetch';
import { useMachine, useInterpret } from '@xstate/vue';
import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue';
import Enhancer from '@/components/show/Enhancer.vue';
import { publicStore } from '@/stores/public';
import {transfromStore} from "@/stores/resTransform"
const fetchM = useInterpret(permachine)
const pollingM = useMachine(machine)
const fetchS = fetchStore()
// const publicS = publicStore()
const transS = transfromStore()
const { getCommonResEffect } = fetchS
// const {getCommonResEffect} = publicStore()
const loop = fetchS.pullResultsLoop(pollingM)
const tableResults = reactive([] as Map<string, string>[])
const keyList = [
"ts",
"cost",
"daemon",
"priority",
"stackTrace",
"classloader",
"threadId",
"threadName",]
// const enhancer = reactive(new Map())
const enhancer = ref(undefined as EnchanceResult | undefined)
getCommonResEffect(pollingM, body => {
if (body.results.length > 0) {
body.results.forEach(result => {
if (result.type === "stack") {
const map = new Map()
Object
.keys(result)
.filter((k) => !["jobId", "type"].includes(k))
.forEach(k => {
let val: string | string[] = ""
if (k === "stackTrace") {
let stackTrace = result[k]
val = stackTrace.map((trace) => transS.transformStackTrace(trace))
} else {
val = result[k as Exclude<keyof typeof result, "jobId" | "type" | "stackTrace">].toString()
}
map.set(k, val)
})
// pollResults.unshift([result.ts, map])
tableResults.unshift(map)
}
if (result.type === "enhancer") {
enhancer.value = result
}
})
}
})
onBeforeMount(() => {
// fetchM.send("INIT")
pollingM.send("INIT")
fetchS.asyncInit()
// loop.open()
})
onBeforeUnmount(() => {
loop.close()
})
const submit = async (data: { classItem: Item, methodItem: Item, conditon: string, count: number }) => {
let className = data.classItem.value
let methodName = data.methodItem.value
let condition = data.conditon.trim() == "" ? "" : `'${data.conditon.trim()}'`
let n = data.count > 0 ? `-n ${data.count}` : ""
// pollResults.length = 0
enhancer.value = undefined
// tableResults.length = 0
fetchS.baseSubmit(fetchM, {
action: "async_exec",
command: `stack ${className} ${methodName} ${condition} ${n}`,
sessionId: undefined
}).then(res => {
loop.open()
})
}
</script>
<template>
<MethodInput :submit-f="submit" ncondition ncount></MethodInput>
<Enhancer :result="enhancer" v-if="enhancer"></Enhancer>
<div class="w-full flex justify-center items-center mt-4">
<table class="table w-full table-compact group">
<thead>
<tr>
<th class="border border-slate-300" v-for="(v,i) in keyList" :key="i" :class="{'group-first:z-0':i===0}">{{v}}</th>
</tr>
</thead>
<tbody class="">
<tr v-for="(map, i) in tableResults" :key="i">
<td class="border border-slate-300" v-for="(key,j) in keyList" :key="j">
<template v-if="key!== 'stackTrace'">
{{map.get(key)}}
</template>
<div class="flex flex-col items-end" v-else>
<div v-for="(row, k) in map.get(key)" :key="k">
{{row}}
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
@@ -0,0 +1,178 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import {
Switch,
SwitchLabel,
SwitchGroup
} from "@headlessui/vue"
import { useInterpret, useMachine } from '@xstate/vue';
import MethodInput from '@/components/input/MethodInput.vue';
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import { onBeforeMount, onBeforeUnmount, reactive, ref } from 'vue';
import Tree from '@/components/show/Tree.vue';
import Enhancer from '@/components/show/Enhancer.vue';
import { publicStore } from '@/stores/public';
const pollingM = useMachine(machine)
const fetchS = fetchStore()
const { pullResultsLoop, getCommonResEffect } = fetchS
const fetchM = useInterpret(permachine)
const loop = pullResultsLoop(pollingM)
const pollResults = reactive<TreeNode[]>([])
const enhancer = ref(undefined as EnchanceResult | undefined)
const publiC = publicStore()
const excludeClass = ref("")
const enabled = ref(true)
const trans = (root: TraceNode, parent: TraceNode | null): string[] => {
let title: (string)[] = []
if (root.type === "throw") {
const lineNumber = root.lineNumber <= 0 ? "" : `#${root.lineNumber}`
title = ["throw:" + root.exception, "lineNumber", lineNumber, `[${root.message}]`]
} else if (root.type === "thread") {
title = [
"ts=" + root.timestamp,
"thread_name=" + root.threadName,
"daemon=" + root.daemon.toString(),
"priority=" + root.priority.toString(),
"threadId=" + root.threadId.toString(), `TCCL=${root.classloader}`]
} else {
const lineNumber = root.lineNumber <= 0 ? "" : `#${root.lineNumber}`
let percentage = ""
if (parent && parent.type === "method") percentage = `${(root.totalCost / parent.totalCost * 100).toFixed(2)}%, `
if (root.times <= 1) {
console.log(
root.cost,
root.totalCost,
)
if (parent && parent.type === "method") percentage = `${(root.cost / parent.totalCost * 100).toFixed(2)}%, `
title = [`[${percentage}${publiC.nanoToMillis(root.cost)}ms]`, lineNumber, `${root.className}:${root.methodName}`]
} else {
if (parent && parent.type === "method") percentage = `${(root.totalCost / parent.totalCost * 100).toFixed(2)}%, `
title = [
`[`,
percentage,
`min=${publiC.nanoToMillis(root.minCost)}ms, max =${publiC.nanoToMillis(root.maxCost)}ms, total=${publiC.nanoToMillis(root.totalCost)}ms, count=${root.times}]`,
lineNumber,
`${root.className}:${root.methodName}`]
}
}
return title
}
/**处理Tree */
const dfs = (root: TraceNode, parent: TraceNode | null): TreeNode => {
return {
children: root.children?.map(child => dfs(child, root)) || [],
meta: trans(root, parent) as string[]
}
}
getCommonResEffect(pollingM, body => {
if (body.results.length > 0) {
body.results.forEach(result => {
if (result.type === "trace") {
// const trans = (root: TraceNode): Map<string, string[]> => {
// /** 用于cmdRes */
// const map = new Map(Object
// .entries(root)
// .filter(([k, v]) => "children" !== k)
// .map(([k, v]) => [k, [v.toString()]]
// )
// ) as Map<string, string[]>
// /**显示简略信息 */
// let title = ""
// if (root.type === "throw") {
// title = "throw"
// } else if (root.type === "thread") {
// title = `${root.timestamp} ${root.threadName}`
// } else {
// title = `[${root.totalCost}ms]${root.className}::${root.methodName}`
// }
// map.set("title", [title])
// return map
// }
const root: TreeNode = {
children: result.root?.children?.map(ch => dfs(ch, null)) || [],
meta: trans(result.root, null)
}
pollResults.unshift(root)
}
if (result.type === "enhancer") {
enhancer.value = result
}
if (result.type === "status") {
console.log(result)
// 自动关停,目前有bug,应为interrupt也会出现statusCode,应该计数,目前还没办法解决
if (result.statusCode === 0) {
// statusCount--
console.log("close!!!")
// loop.close()
}
}
})
}
})
onBeforeMount(() => {
pollingM.send("INIT")
fetchS.asyncInit()
})
onBeforeUnmount(() => {
loop.close()
})
const setExclude = publicStore().inputDialogFactory(excludeClass,
(raw) => raw,
(input) => input.value.toString()
)
const submit = (data: { classItem: Item, methodItem: Item, conditon: string, count: number }) => {
let condition = data.conditon.trim() == "" ? "" : `'${data.conditon.trim()}'`
// let express = data.express.trim() == "" ? "" : `'${data.express.trim()}'`
let n = data.count > 0 ? `-n ${data.count}` : ""
let exclude = excludeClass.value == "" ? "" : `--exclude-class-pattern ${excludeClass.value}`
let method = data.methodItem.value === "" ? "*" : data.methodItem.value
let skipJDKMethod = enabled.value ? "" : "--skipJDKMethod false"
return fetchS.baseSubmit(fetchM, {
action: "async_exec",
command: `trace ${data.classItem.value} ${method} ${skipJDKMethod} ${condition} ${n} ${exclude}`,
sessionId: undefined
}).then(() => {
enhancer.value = undefined
pollResults.length = 0
loop.open()
})
}
</script>
<template>
<MethodInput :submit-f="submit" class="mb-2" ncondition ncount>
<template #others>
<label class="label cursor-pointer btn-sm border border-neutral ml-2">
<span class="label-text uppercase font-bold mr-1">skip JDK Method</span>
<input v-model="enabled" type="checkbox" class="toggle"/>
</label>
<button class="btn btn-outline btn-sm ml-2" @click="setExclude">exclude: {{excludeClass}}</button>
</template>
</MethodInput>
<template v-if="pollResults.length > 0 || enhancer">
<Enhancer :result="enhancer" v-if="enhancer"></Enhancer>
<ul class=" pointer-events-auto mt-2">
<template v-for="(result, i) in pollResults" :key="i">
<Tree :root="result" class=" border-t-2 mb-4 pt-4">
<!-- 具体信息的表达 -->
<template #meta="{ data, active }">
<div class="bg-info p-1 mb-1 rounded-r rounded-br">
{{data.join(" ")}}
</div>
</template>
</Tree>
</template>
</ul>
</template>
</template>
@@ -0,0 +1,367 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { useInterpret, useMachine } from '@xstate/vue';
import MethodInput from '@/components/input/MethodInput.vue';
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import { onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, } from 'vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import Enhancer from '@/components/show/Enhancer.vue';
import { publicStore } from '@/stores/public';
import * as echarts from 'echarts/core';
import {
TitleComponent,
TitleComponentOption,
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
DataZoomComponent,
DataZoomComponentOption
} from 'echarts/components';
import {
BarChart,
BarSeriesOption,
LineChart,
LineSeriesOption
} from 'echarts/charts';
import {
UniversalTransition
} from 'echarts/features';
import {
SVGRenderer
} from 'echarts/renderers';
import { ECharts, number } from 'echarts/core';
echarts.use(
[TitleComponent, ToolboxComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, BarChart, LineChart, SVGRenderer, UniversalTransition]
);
type EChartsOption = echarts.ComposeOption<
TitleComponentOption | ToolboxComponentOption | TooltipComponentOption | GridComponentOption | LegendComponentOption | DataZoomComponentOption | BarSeriesOption | LineSeriesOption
>
const pollingM = useMachine(machine)
const fetchS = fetchStore()
const { pullResultsLoop, getPullResultsEffect } = fetchS
const fetchM = useInterpret(permachine)
const loop = pullResultsLoop(pollingM)
const enhancer = ref(undefined as EnchanceResult | undefined)
const trigerRes = reactive(new Map<string, string[]>)
const cacheIdx = ref("-1")
const inputVal = ref("")
const keyList: tfkey[] = [
"index",
"timestamp",
"className",
"methodName",
"cost",
"object",
"params",
"returnObj",
"throwExp",
// 暂时隐藏这两个属性,不够宽了
// "return",
// "throw",
]
const tableResults = reactive([] as Map<string, string>[])
// const timeFragmentSet = new Set()
type tfkey = keyof TimeFragment
const chartContext: {
count: number,
myChart?: ECharts,
categories: number[],
data: number[],
cur: number,
max: number,
} = {
max: 0,
cur: 0,
count: 20,
myChart: undefined,
categories: [],
data: []
}
const chartOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#283b56'
}
}
},
legend: {},
toolbox: {
show: true,
feature: {
dataView: { readOnly: false },
}
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: chartContext.categories
}
],
yAxis: [
{
type: 'value',
scale: true,
name: 'cost(ms)',
max: 0,
min: 0,
boundaryGap: [0.2, 0.2]
}
],
series: [
{
name: 'cost',
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 0,
data: chartContext.data
}
]
};
const updateChart = (tf: TimeFragment) => {
while (chartContext.cur > chartContext.count) {
chartContext.data.shift()
chartContext.categories.shift()
chartContext.cur--
}
chartContext.data.push(tf.cost)
chartContext.categories.push(tf.index)
chartContext.cur++
chartContext.max = Math.max(...chartContext.data)
chartContext.myChart!.setOption<EChartsOption>({
xAxis: [
{
data: chartContext.categories
}
],
yAxis: [
{
max: chartContext.max,
}
],
series: [
{
data: chartContext.data
}
]
});
}
const transform = (tf: TimeFragment) => {
const map = new Map()
Object.keys(tf).forEach((k) => {
let val: string | string[] = []
if ((k) === "params") {
tf.params.forEach(para => {
// 以后可能会有bug
for (const key in para) {
// @ts-ignore
val.push(`${key}:${para[key].toString()}`)
}
})
} else {
val = (tf[k as tfkey].toString())
}
map.set(k, val)
})
updateChart(tf)
return map
}
getPullResultsEffect(
pollingM,
result => {
if (result.type === "tt") {
result.timeFragmentList.forEach(tf => {
console.log(tf.index)
// if(!timeFragmentSet.has(tf.index)){
// timeFragmentSet.add(tf.index)
tableResults.unshift(transform(tf))
// }
})
}
if (result.type == "enhancer") {
enhancer.value = result
}
})
onBeforeMount(() => {
pollingM.send("INIT")
fetchS.asyncInit()
})
onMounted(() => {
const chartDom = document.getElementById('ttchart')!;
chartContext.myChart = echarts.init(chartDom);
chartContext.myChart.on("click", e => {
console.dir(e)
})
chartOption && chartContext.myChart.setOption(chartOption)
})
onBeforeUnmount(() => {
loop.close()
})
const submit = async (data: { classItem: Item, methodItem: Item, count: number, conditon: string }) => {
let n = data.count > 0 ? `-n ${data.count}` : ""
let condition = data.conditon
fetchS.baseSubmit(fetchM, {
action: "async_exec",
command: `tt -t ${data.classItem.value} ${data.methodItem.value} ${n} ${condition}`,
sessionId: undefined
}).then(res => {
enhancer.value = undefined
loop.open()
})
}
const alltt = () => fetchS.baseSubmit(fetchM, {
action: "exec",
command: `tt -l`
}).then((res) => {
let result = (res as CommonRes).body.results[0]
trigerRes.clear()
tableResults.length = 0
if (result.type === "tt") {
result.timeFragmentList.forEach(tf => {
tableResults.unshift(transform(tf))
})
}
})
const reTrigger = (idx: string) => fetchS.baseSubmit(fetchM, {
action: "exec",
command: `tt -i ${idx} -p`,
}).then(
res => {
let result = (res as CommonRes).body.results[0]
if (result.type === "tt") {
trigerRes.clear()
cacheIdx.value = idx
let tf = result.replayResult
Object.keys(tf).forEach((k) => {
let val: string[] = []
if ((k as keyof TimeFragment) === "params") {
tf.params.forEach(para => {
val.push(JSON.stringify(para))
})
} else {
val.push(tf[k as keyof TimeFragment].toString())
}
trigerRes.set(k as tfkey, val)
})
trigerRes.set("sizeLimit", [result.sizeLimit.toString()])
trigerRes.set("replayNo", [result.replayNo.toString()])
}
}, () => {
trigerRes.clear()
}
)
const searchTt = () => {
let condition = inputVal.value.trim() !== "" ? `'${inputVal.value}'` : ''
return fetchS.baseSubmit(fetchM, {
action: "exec",
command: `tt -s ${condition}`
}).then(res => {
tableResults.length = 0
trigerRes.clear()
let result = (res as CommonRes).body.results[0]
if (result.type === "tt") {
if (result.timeFragmentList.length === 0) {
publicStore().$patch({
isErr: true,
ErrMessage: "not found"
})
return
}
result.timeFragmentList.forEach(tf => {
tableResults.unshift(transform(tf))
})
}
}).catch(err=>{
console.error(err)
})
}
</script>
<template>
<MethodInput :submit-f="submit" ncount ncondition>
</MethodInput>
<div class="divider"></div>
<div class="flex items-center justify-between">
<div class="mr-2">searching records</div>
<div
class="flex-1 overflow-hidden rounded-lg bg-white text-left border focus-within:outline outline-2 hover:shadow-md transition">
<input type="text" v-model="inputVal"
class="w-full border-none py-2 pl-3 pr-10 h-full text-gray-900 focus:outline-none">
</div>
<button @click="searchTt" class="mx-2 btn btn-primary btn-sm btn-outline">search</button>
</div>
<div class="flex justify-end">
<button class="btn btn-primary btn-sm btn-outline my-4 mr-2" @click="alltt">
all records
</button>
</div>
<div class="pointer-events-auto">
<div id="ttchart" class="w-full h-60 input-btn-style mb-4"></div>
<div class="text-gray-500">
<CmdResMenu title="invoked result" :map="trigerRes" v-if="trigerRes.size > 0">
<template #headerAside>
<div class="flex mt-2 justify-end mr-1">
<button @click="reTrigger(cacheIdx)" class="btn btn-primary btn-outline btn-xs p-1">invoke</button>
</div>
</template>
</CmdResMenu>
</div>
<!-- <template v-if="enhancer|| tableResults.length > 0"> -->
<Enhancer :result="enhancer" v-if="enhancer"></Enhancer>
<div class="w-full flex justify-center items-center overflow-auto flex-1">
<table class="table table-compact group w-full">
<thead>
<tr>
<th class="border group-first:z-0" v-for="(v,i) in keyList" :key="i">{{v}}</th>
<th class="border">invoke</th>
</tr>
</thead>
<tbody class="">
<tr v-for="(map, i) in tableResults" :key="i">
<td class="border" v-for="(key,j) in keyList" :key="j">
<template v-if=" key !== 'params'">
{{map.get(key)}}
</template>
<div class="flex flex-col" v-else>
<div v-for="(row, k) in map.get(key)" :key="k">
{{row}}
</div>
</div>
</td>
<td class="border">
<button class="btn btn-primary btn-sm btn-outline" @click="reTrigger(map.get('index')!)">invoke</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- </template> -->
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,245 @@
<script setup lang="ts">
import permachine from '@/machines/perRequestMachine';
import { useInterpret, useMachine } from '@xstate/vue';
import MethodInput from '@/components/input/MethodInput.vue';
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Switch,
SwitchLabel,
SwitchGroup,
SwitchDescription
} from "@headlessui/vue"
import { onBeforeMount, onBeforeUnmount, reactive, Ref, ref, watchEffect } from 'vue';
import Tree from '@/components/show/Tree.vue';
import Enhancer from '@/components/show/Enhancer.vue';
import { publicStore } from '@/stores/public';
const pollingM = useMachine(machine)
const fetchS = fetchStore()
const publiC = publicStore()
const { pullResultsLoop, getPullResultsEffect } = fetchS
const fetchM = useInterpret(permachine)
const loop = pullResultsLoop(pollingM)
const pollResults = reactive([] as [string, Map<string, string[]>, TreeNode][])
const enhancer = ref(undefined as EnchanceResult | undefined)
const depth = ref(1)
const tableResults = reactive([] as Map<string, string | TreeNode>[])
const keyList = [
"ts",
"accessPoint",
"className",
"methodName",
"cost",
// "sizeLimit",
"value",
]
const tranOgnl = (s: string): string[] => s.split("\n")
// type Mode = "-f" | "-s" | "-e" | "-b"
// const modelist: { name: string, value: Mode }[] = [
// { name: "before method being invoked", value: "-b" },
// { name: "when method encountering exceptions", value: "-e" },
// { name: "when method exits normally", value: "-s" },
// { name: "when method exits", value: "-f" }
// ]
const beforeInvoke = ref(false)
const successInvoke = ref(false)
const failureInvoke = ref(false)
const allInvoke = ref(true)
const modereflist: { enabled: Ref<boolean>, name: string }[] = [
{ enabled: beforeInvoke, name: "before" },
{ enabled: successInvoke, name: "success" },
{ enabled: failureInvoke, name: "exception" },
{ enabled: allInvoke, name: "finish" }
]
const selectedMode: { enabled: Ref<boolean>, name: string }[] = [
{ enabled: beforeInvoke, name: "before" },
{ enabled: successInvoke, name: "success" },
{ enabled: failureInvoke, name: "exception" },
{ enabled: allInvoke, name: "finish" }
]
// const mode = ref(modelist[3])
const transform = (result: CommandResult) => {
const map = new Map();
if (result.type !== "watch") return map
for (const key in result) {
if (key !== "value") {
//@ts-ignore
map.set(key, result[key])
}
}
let raw = tranOgnl(result.value)
const stk: TreeNode[] = []
// Tree的构建
raw.forEach(v => {
let str = v.trim()
let match = 0
for (let s of str) {
if (s === "[") {
match++
} else if (s === "]") {
match--
}
}
const root = {
children: [],
meta: str.substring(0, str.length - 1)
} as TreeNode
if (match > 0) {
stk.push(root)
} else if (match === 0) {
let cur = stk.pop()
if (cur) {
cur.children!.push(root)
stk.push(cur)
} else {
stk.push(root)
}
} else {
/// 默认每行只会一个]
//!可能会有bug
let cur = stk.pop()!
if (stk.length > 0) {
let parent = stk.pop()!
parent.children!.push(cur)
stk.push(parent)
} else {
// 构建结束
stk.push(cur)
}
}
})
map.set("value", stk[0])
return map
}
getPullResultsEffect(
pollingM,
result => {
console.log(result)
if (result.type === "watch") {
tableResults.unshift(transform(result))
}
if (result.type === "enhancer") {
enhancer.value = result
}
})
const setDepth = publiC.inputDialogFactory(
depth,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 1 : valRaw
},
(input) => input.value.toString(),
)
onBeforeMount(() => {
pollingM.send("INIT")
fetchS.asyncInit()
})
onBeforeUnmount(() => {
loop.close()
})
const submit = async (data: { classItem: Item, methodItem: Item, conditon: string, express: string }) => {
let conditon = data.conditon.trim() == "" ? "" : `'${data.conditon.trim()}'`
let express = data.express.trim() == "" ? "" : `'${data.express.trim()}'`
let mode = ""
if (beforeInvoke.value) mode += " -b"
if (failureInvoke.value) mode += " -e"
if (successInvoke.value) mode += " -s"
if (allInvoke.value) mode += " -f"
tableResults.length = 0
fetchS.baseSubmit(fetchM, {
action: "async_exec",
command: `watch ${mode} ${data.classItem.value} ${data.methodItem.value} -x ${depth.value} ${conditon} ${express}`,
sessionId: undefined
}).finally(() => {
pollResults.length = 0
enhancer.value = undefined
loop.open()
})
}
</script>
<template>
<MethodInput :submit-f="submit" nexpress ncondition>
<template #others>
<div class="relative group ml-2">
<div class="btn btn-sm btn-outline">watching point</div>
<div class="h-0 group-hover:h-auto group-focus-within:h-auto absolute overflow-clip transition z-10 top-full pt-2">
<!-- <SwitchGroup v-for="(mode,i) in modereflist" :key="i">
<div class="flex input-btn-style ml-2 focus-within:outline outline-1 justify-between m-2 bg-white">
<SwitchLabel class="mr-2">{{mode.name}}:</SwitchLabel>
<Switch v-model="mode.enabled.value" :class="mode.enabled.value ? 'bg-blue-400' : 'bg-gray-500'"
class="relative items-center inline-flex h-6 w-12 shrink-0 cursor-pointer rounded-full border-transparent transition-colors ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 mr-2">
<span aria-hidden="true" :class="mode.enabled.value ? 'translate-x-6' : '-translate-x-1'"
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow-md shadow-gray-500 ring-0 transition ease-in-out" />
</Switch>
</div>
</SwitchGroup> -->
<label class="label cursor-pointer btn-sm border border-neutral ml-2 bg-base-100"
v-for="(mode,i) in modereflist" :key="i">
<span class="label-text uppercase font-bold mr-1">{{mode.name}}</span>
<input v-model="mode.enabled.value" type="checkbox" class="toggle" />
</label>
</div>
</div>
<!-- <Listbox v-model="modereflist" multiple class="relative" as="div">
<ListboxButton as="button" class="btn btn-sm btn-outline ml-2">
watching point
</ListboxButton>
<ListboxOptions class="absolute top-full z-10">
<ListboxOption v-for="mode in modereflist" :key="mode.name" :value="mode" v-slot="{active}">
<label class="label cursor-pointer bg-base-100">
<span class="label-text">{{mode.name}}</span>
<input v-model="mode.enabled.value" type="checkbox" class="toggle" />
</label>
</ListboxOption>
</ListboxOptions>
</Listbox> -->
<button class="btn btn-sm btn-outline ml-2" @click="setDepth">depth:{{depth}}</button>
</template>
</MethodInput>
<Enhancer :result="enhancer" v-if="enhancer"></Enhancer>
<div class="flex justify-center mt-4 overflow-auto">
<table class="table w-full group">
<thead>
<tr>
<th class="border border-slate-300" v-for="(v,i) in keyList" :key="i" :class="{'group-first:z-0':i===0}">{{v}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(map, i) in tableResults" :key="i">
<td class="border border-slate-300" v-for="(key,j) in keyList" :key="j">
<div v-if=" key !== 'value'">
{{map.get(key)}}
</div>
<div class="flex flex-col" v-else>
<Tree :root="(map.get('value') as TreeNode)" class="mt-2" button-class=" ">
<template #meta="{ data, active }">
<div class="bg-blue-200 p-2 mb-2 rounded-r rounded-br"
:class='{"hover:bg-blue-300 bg-blue-400":active}'>
{{data}}
</div>
</template>
</Tree>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
@@ -0,0 +1,175 @@
<script setup lang="ts">import machine from '@/machines/consoleMachine';
import { publicStore } from '@/stores/public';
import { useMachine } from '@xstate/vue';
import { reactive } from 'vue';
import ClassInput from '@/components/input/ClassInput.vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { fetchStore } from '@/stores/fetch';
import { interpret } from 'xstate';
import permachine from '@/machines/perRequestMachine';
const classInfoM = useMachine(machine)
const classMethodInfoM = useMachine(machine)
const dumpM = useMachine(machine)
const classDetailMap = reactive(new Map<string, string[]>())
const classFields = reactive(new Map<string, string[]>())
const classMethodMap = reactive(new Map<string, string[]>())
const dumpMap = reactive(new Map<string, string[]>())
const { getCommonResEffect } = publicStore()
const publicS = publicStore()
const fetchS = fetchStore()
// getCommonResEffect(classInfoM, body => {
// const result = body.results[0]
// if (result.type === "sc" && result.detailed === true && result.withField === true) {
// classDetailMap.clear()
// classFields.clear()
// Object.entries(result.classInfo).filter(([k, v]) => k !== "fields").forEach(([k, v]) => {
// let value: string[] = []
// if (!["interfaces", "annotations", "classloader", "superClass"].includes(k)) value.push(v.toString())
// else value = v as string[]
// classDetailMap.set(k, value)
// })
// result.classInfo.fields.forEach(field => {
// classFields.set(field.name, Object.entries(field).filter(([k, v]) => k !== "name").map(([k, v]) => {
// if (k === "value") v = JSON.stringify(v)
// return `${k}: ${v}`
// }))
// })
// }
// })
getCommonResEffect(classMethodInfoM, body => {
classMethodMap.clear()
body.results.forEach(result => {
if (result.type === "sm" && result.detail == true) {
classMethodMap.set(result.methodInfo.methodName, Object.entries(result.methodInfo).filter(([k, v]) => k !== "methodName").map(([k, v]) => {
let res = k + ' : '
if (!["exceptions", "parameters", "annotations"].includes(k)) res += v.toString()
else res += JSON.stringify(v)
return res
}))
}
})
})
getCommonResEffect(dumpM, body => {
dumpMap.clear()
body.results.forEach(result => {
if (result.type === "dump") {
result.dumpedClasses.forEach(obj => {
dumpMap.set(obj.name, Object.entries(obj).filter(([k, v]) => k !== "name").map(([k, v]) => {
let res = k + ' : '
if (k === "classloader") res += JSON.stringify(v)
else res += v
return res
}))
})
}
})
})
const getClassInfo = (data: { classItem: Item; loaderItem: Item }) => {
let item = data.classItem
let classLoader = data.loaderItem.value === "" ? "" : `-c ${data.loaderItem.value}`
classDetailMap.clear()
classFields.clear()
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `sc -d -f ${item.value} ${classLoader}`
}).then(
res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "sc" && result.detailed === true && result.withField === true) {
Object.entries(result.classInfo).filter(([k, v]) => k !== "fields").forEach(([k, v]) => {
let value: string[] = []
if (!["interfaces", "annotations", "classloader", "superClass"].includes(k)) value.push(v.toString())
else value = v as string[]
classDetailMap.set(k, value)
})
result.classInfo.fields.forEach(field => {
classFields.set(field.name, Object.entries(field).filter(([k, v]) => k !== "name").map(([k, v]) => {
if (k === "value") v = JSON.stringify(v)
return `${k}: ${v}`
}))
})
}
}
)
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `sm -d ${item.value} ${classLoader}`
}).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "sm" && result.detail == true) {
classMethodMap.set(result.methodInfo.methodName, Object.entries(result.methodInfo).filter(([k, v]) => k !== "methodName").map(([k, v]) => {
let res = k + ' : '
if (!["exceptions", "parameters", "annotations"].includes(k)) res += v.toString()
else res += JSON.stringify(v)
return res
}))
}
})
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `dump ${item.value} ${classLoader}`
}).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "dump") {
result.dumpedClasses.forEach(obj => {
dumpMap.set(obj.name, Object.entries(obj).filter(([k, v]) => k !== "name").map(([k, v]) => {
let res = k + ' : '
if (k === "classloader") res += JSON.stringify(v)
else res += v
return res
}))
})
}
})
// classInfoM.send({
// type: "SUBMIT",
// value: {
// action: "exec",
// command: `sc -d -f ${item.value} ${classLoader}`
// }
// })
// classMethodInfoM.send({
// type: "SUBMIT",
// value: {
// action: "exec",
// command: `sm -d ${item.value} ${classLoader}`
// }
// })
// dumpM.send({
// type: "SUBMIT",
// value: {
// action: "exec",
// command: `dump ${item.value} ${classLoader}`
// }
// })
}
</script>
<template>
<ClassInput :submit-f="getClassInfo"></ClassInput>
<div>
<template v-if="classDetailMap.size !== 0">
<h4 class="grid place-content-center mb-2 text-3xl mt-4">classInfo</h4>
<CmdResMenu :map="classFields" title="fields"></CmdResMenu>
<CmdResMenu :map="classDetailMap" title="detail"></CmdResMenu>
</template>
<template v-if="classMethodMap.size !== 0">
<CmdResMenu :map="classMethodMap" title="methods"></CmdResMenu>
</template>
<template v-if="dumpMap.size !== 0">
<CmdResMenu :map="dumpMap" title="dump"></CmdResMenu>
</template>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,422 @@
<script setup lang="ts">
import { computed, onBeforeMount, onUnmounted, reactive, ref } from 'vue';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
import { publicStore } from "@/stores/public"
import { fetchStore } from '@/stores/fetch';
import { interpret } from 'xstate';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import transformMachine from '@/machines/transformConfigMachine';
// import ClassInput from '@/components/input/ClassInput.vue';
import permachine from '@/machines/perRequestMachine';
import Tree from '@/components/show/Tree.vue';
const fetchS = fetchStore()
const urlStats = ref([] as [
string,
Map<"hash" | "unUsedUrls" | "usedUrls" | "parent", string[]>,
string
][])
// const tablelResults = reactive([] as Map<string, string | number>[])
const tableResults = reactive([] as Map<string, string | number>[])
const loaderCache = ref({ name: "", hash: "", count: "" } as Record<"name" | "hash" | "count", string>)
const classLoaderTree = reactive([] as TreeNode[])
const hashCode = computed(() => {
let res = loaderCache.value.hash.trim() === "" ? "" : "-c " + loaderCache.value.hash.trim()
if (loaderCache.value.hash.trim() === "null") {
res = `--classLoaderClass ${loaderCache.value.name}`
}
return res
})
const selectedClassLoadersUrlStats = ref([] as string[])
const classVal = ref("")
const resourceVal = ref("")
// const keylList = [
// "name", "loadedCount", "hash", "parent"
// ]
const keyList = [
"name", "numberOfInstance", "loadedCount"
]
const trans = (root: ClassLoaderNode, parent: ClassLoaderNode | null): string[] => {
let title: (string)[] = []
let count = root.loadedCount.toString()
let name = root.name.split('@')[0]
let hash = root.hash
title = [count, name, hash]
return title
}
/**处理Tree */
const dfs = (root: ClassLoaderNode, parent: ClassLoaderNode | null): TreeNode => {
let children: TreeNode[] = []
if ("children" in root) {
if (root.children) {
children = root.children.map(child => dfs(child, root))
}
}
return {
children,
meta: trans(root, parent) as string[]
}
}
const json_to_obj = (str: string) => {
const actor = interpret(transformMachine)
actor.start()
actor.send("INPUT", {
data: str
})
return fetchS.isResult(actor).then(
state => {
if (state.matches("success")) {
return Promise.resolve(state.context.output)
} else {
publicStore().$patch({
isErr: true,
ErrMessage: actor.state.context.err
})
return Promise.reject(1)
}
}
).catch(
err => {
return Promise.reject(2)
}
)
}
const getAllUrlStats = () => fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: "classloader --url-stat"
}).then(res => {
let result = (res as CommonRes).body.results[0]
if (result.type === "classloader" && Object.hasOwn(result, "urlStats")) {
urlStats.value.length = 0
Object.entries(result.urlStats).forEach(([k, v]) => {
json_to_obj(k).then(
obj => {
urlStats.value.push([
obj.name.split("@")[0],
new Map([
["parent", [obj.parent]],
["hash", [obj.hash]],
["unUsedUrls", v.unUsedUrls],
["usedUrls", v.usedUrls]
]),
obj.hash
])
}
).catch(err => {
console.error(err)
})
})
}
})
const getClassLoaderTree = () => fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: "classloader -t"
}).then(res => {
const results = (res as CommonRes).body.results
classLoaderTree.length = 0
results.forEach(result => {
if (result.type === "classloader" && result.tree) {
result.classLoaders.forEach(classloader => {
classLoaderTree.push(dfs(classloader, null))
})
}
})
}, err => {
console.error(err)
})
// const getCategorizedByLoaded = () => {
// tablelResults.length = 0
// fetchS.baseSubmit(interpret(permachine), {
// action: "exec",
// command: "classloader -l"
// }).then(res => {
// const result = (res as CommonRes).body.results[0]
// if (result.type === "classloader" && !result.tree) {
// result.classLoaders.forEach(loader => {
// const map = new Map()
// for (const key in loader) {
// //@ts-ignore
// if(key == "name") map.set(key, loader[key].split("@")[0])
// else map.set(key, loader[key])
// }
// tablelResults.push(map)
// })
// }
// })
// }
const getCategorizedByClassType = () => {
tableResults.length = 0
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: "classloader"
}).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "classloader") {
for (const name in result.classLoaderStats) {
const map = new Map()
for (const key in result.classLoaderStats[name]) {
map.set(key, result.classLoaderStats[name][key])
}
map.set("name", name)
tableResults.push(map)
}
}
})
}
onBeforeMount(() => {
getAllUrlStats()
getClassLoaderTree()
// getCategorizedByLoaded()
getCategorizedByClassType()
})
const loadClass = () => {
let classItem = classVal.value.trim() === "" ? "" : `--load ${classVal.value.trim()}`
if (classItem === "") return
return fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `classloader ${hashCode.value} ${classItem}`
}).then(res => {
let result = (res as CommonRes).body.results[0]
publicStore().$patch({
isSuccess: true,
SuccessMessage: JSON.stringify(result)
})
})
}
const loadResource = () => {
let resourceItem = resourceVal.value.trim() === "" ? "" : `-r ${resourceVal.value.trim()}`
if (resourceItem === "") return
return fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `classloader ${hashCode.value} ${resourceItem}`
}).then(res => {
let result = (res as CommonRes).body.results[0]
publicStore().$patch({
isSuccess: true,
SuccessMessage: JSON.stringify(result)
})
})
}
const getUrlStats = () => {
selectedClassLoadersUrlStats.value = []
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `classloader ${hashCode.value}`
}).then(res => {
let result = (res as CommonRes).body.results[0]
if (result.type === "classloader") {
selectedClassLoadersUrlStats.value = result.urls
}
})
}
const selectClassLoader = (data: { hash: string, name: string, count: string }) => {
loaderCache.value = data
getUrlStats()
}
const resetClassloader = () => {
selectClassLoader({ hash: "", name: "", count: "" })
}
</script>
<template>
<div class="flex flex-col h-full justify-between">
<div class="flex h-[40vh]">
<div class="input-btn-style h-full p-4 mb-2 flex flex-col transition-all duration-500" :class='{
"w-full":loaderCache.hash === "",
"w-2/3":loaderCache.hash !== ""
}'>
<!-- 后置为了让用户能注意到右上角的refreshicon -->
<div class="h-[5vh] mb-4 justify-end flex">
<button @click="resetClassloader" class="btn btn-primary btn-sm mr-1">reset</button>
<button @click="getClassLoaderTree" class="btn btn-primary btn-sm">refresh</button>
</div>
<div class="overflow-auto w-full flex-1">
<div v-for="(tree,i) in classLoaderTree" :key="i">
<Tree :root="tree">
<template #meta="{ data, active }">
<!-- <div class="flex items-center"> -->
<div class="bg-info px-2 rounded-r rounded-br mr-2 text-info-content" :class='{
"hover:opacity-50":active,
"bg-success text-success-content":loaderCache.hash=== data[2]
}'>
{{data[1]}}
<!-- </div> -->
</div>
</template>
<template #others="{data}">
<div class="items-center flex">
<div class="mr-2">
<span class="bg-primary-focus text-primary-content border border-primary-focus px-2 rounded-l ">
count :
</span>
<span class="bg-base-200 border border-primary-focus rounded-r px-1 ">
{{data[0]}}
</span>
</div>
<div class="mr-2">
<span class="bg-primary-focus px-2 rounded-l text-primary-content border border-primary-focus">
hash :
</span>
<span class="bg-base-200 rounded-r flex-1 px-1 border border-primary-focus">
{{data[2]}}
</span>
</div>
<!-- <div class="">count:{{data[0]}}</div> -->
<button @click="selectClassLoader({name:data[1],hash:data[2],count:data[0]})" class="btn btn-primary btn-xs btn-outline opacity-0 group-hover:opacity-100"
v-if="data[2]!== 'null'">
select classloader
</button>
</div>
</template>
</Tree>
</div>
</div>
</div>
<div class="w-1/3 ml-2 overflow-y-scroll transition-all duration-500" :class='{
"w-0":loaderCache.hash === "",
"input-btn-style":loaderCache.hash !==""
}'>
<!-- <div class="overflow-auto h-full"> -->
<div class="mb-2">
<div class="overflow-auto">
<span class="bg-primary-focus px-2 rounded-l text-primary-content border border-primary-focus">
selected classLoader:
</span>
<span class="bg-base-200 rounded-r px-1 border border-primary-focus">
{{loaderCache.name}}
</span>
</div>
<div class="mr-2">
<span class="bg-primary-focus px-2 rounded-l text-primary-content border border-primary-focus">
loadedcount :
</span>
<span class="bg-base-200 rounded-r px-1 border border-primary-focus">
{{loaderCache.count}}
</span>
</div>
<div class="mr-2">
<span class="bg-primary-focus px-2 rounded-l text-primary-content border border-primary-focus">
hash :
</span>
<span class="bg-base-200 rounded-r px-1 border border-primary-focus">
{{loaderCache.hash}}
</span>
</div>
</div>
<template v-if="loaderCache.hash.trim() !== ''">
<div class="flex mb-2 w-full">
<div class=" cursor-default
flex-auto
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline outline-2
hover:shadow-md transition mr-2">
<input class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none"
v-model="classVal" />
</div>
<button @click="loadClass" class="btn btn-primary btn-sm btn-outline">load class</button>
</div>
<div class="flex w-full">
<div class=" cursor-default
flex-auto
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline outline-2
hover:shadow-md transition mr-2">
<input class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none"
v-model="resourceVal" />
</div>
<button @click="loadResource" class="btn btn-primary btn-sm btn-outline">load resource</button>
</div>
<div class="h-0 border my-2"></div>
<div class="flex justify-between">
<h3 class="text-xl flex-1 flex justify-center">urls</h3><button class="btn btn-primary btn-sm"
@click="getUrlStats">refresh</button>
</div>
<ul class="mt-2 w-full flex flex-col">
<li v-for="(url,i) in selectedClassLoadersUrlStats" :key="i" class="bg-blue-200 mb-2 p-2 break-all w-full">
{{url}}</li>
</ul>
</template>
<!-- </div> -->
</div>
</div>
<!-- 下面的3格 -->
<div class="w-full flex-auto flex h-[40vh] mt-2">
<div class="input-btn-style w-1/3 mr-2 h-full flex">
<div class="overflow-y-scroll h-ful w-full">
<div class=" mb-2 flex items-center justify-end">
<h3 class="text-xl flex-1 flex justify-center">urlStats</h3>
<button class="btn btn-primary btn-sm" @click="getAllUrlStats">refresh</button>
</div>
<div v-for="v in urlStats" :key="v[0]" class="flex flex-col">
<CmdResMenu :title="v[0]" :map="v[1]" button-width="w-full" :button-accent="v[2] === loaderCache.hash">
</CmdResMenu>
</div>
</div>
</div>
<div class="flex flex-col h-full w-2/3">
<!-- <div class="input-btn-style w-full mr-2 h-1/2">
<div class="overflow-auto flex-1 h-full">
<div class="flex justify-end mb-2">
<button class="button-style" @click="getCategorizedByLoaded">refresh</button>
</div>
<table class="border-collapse border border-slate-400 mx-auto">
<thead>
<tr>
<th class="border border-slate-300 p-2" v-for="(v,i) in keylList" :key="i">{{v}}</th>
</tr>
</thead>
<tbody class="">
<tr v-for="(map, i) in tablelResults" :key="i">
<td class="border border-slate-300 p-2" v-for="(key,j) in keylList" :key="j">
{{map.get(key)}}
</td>
</tr>
</tbody>
</table>
</div>
</div> -->
<div class="input-btn-style w-full mr-2 h-full">
<div class="overflow-auto flex-1 h-full">
<div class="flex justify-end mb-2">
<button class="btn btn-primary btn-sm" @click="getCategorizedByClassType">refresh</button>
</div>
<div class="overflow-auto">
<table class="table w-full group table-compact">
<thead>
<tr>
<th class="border border-slate-300 p-2" v-for="(v,i) in keyList" :key="i" :class="{'group-first:z-0':i==0}">{{v}}</th>
</tr>
</thead>
<tbody class="">
<tr v-for="(map, i) in tableResults" :key="i">
<td class="border border-slate-300 p-2" v-for="(key,j) in keyList" :key="j">
{{map.get(key)}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { publicStore } from '@/stores/public';
import { useMachine } from '@xstate/vue';
import { onBeforeMount, reactive, ref } from 'vue';
import { Switch } from "@headlessui/vue"
import CmdResMenu from '@/components/show/CmdResMenu.vue';
const { getCommonResEffect } = publicStore()
const fetchM = useMachine(machine)
const path = ref("")
const enabled = ref(false)
const map = reactive(new Map())
onBeforeMount(() => {
fetchM.send("INIT")
})
getCommonResEffect(fetchM, body => {
const result = body.results.filter(result => result.type === "heapdump")[0]
if (result.type === "heapdump") {
map.clear()
map.set("filePath", [result.dumpFile])
map.set("live", [result.live])
}
})
const submitCommand = (e: Event) => {
fetchM.send({
type: "SUBMIT",
value: {
action: "exec",
command: `heapdump ${enabled.value ? "--live" : ''}${path.value}`
}
})
}
</script>
<template>
<form class="mb-4 flex items-center justify-between">
<label class="flex flex-1 items-center"> path
<div class="flex-1
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline
outline-2
min-w-[15rem]
mx-2
hover:shadow-md transition">
<input type="text" v-model="path"
class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none">
</div>
<!-- <div class="flex input-btn-style mr-2 focus-within:outline outline-2">
<div class="mx-2">only live object : </div>
<Switch v-model="enabled" :class="enabled ? 'bg-blue-400' : 'bg-gray-500'"
class="relative items-center inline-flex h-6 w-12 shrink-0 cursor-pointer rounded-full border-transparent transition-colors ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 mr-2">
<span aria-hidden="true" :class="enabled ? 'translate-x-6' : '-translate-x-1'"
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow-md shadow-gray-500 ring-0 transition ease-in-out" />
</Switch>
</div> -->
<label class="label cursor-pointer btn-sm border border-neutral mr-2">
<span class="label-text uppercase font-bold mr-1">only live object</span>
<input v-model="enabled" type="checkbox" class="toggle" />
</label>
</label>
<button @click.prevent="submitCommand"
class="btn btn-primary btn-sm btn-outline transition-all truncate p-2">dump</button>
</form>
<CmdResMenu title="dumpRes" open :map="map" v-if="map.size > 0" class="mt-4"></CmdResMenu>
</template>
<style scoped>
</style>
@@ -0,0 +1,58 @@
<script setup lang="ts">
import 'highlight.js/lib/common';
import highlightjsP from "@highlightjs/vue-plugin";
import ClassInput from '@/components/input/ClassInput.vue';
import { useMachine } from '@xstate/vue';
import machine from '@/machines/consoleMachine';
import { onBeforeMount, reactive, ref } from 'vue';
import { publicStore } from '@/stores/public';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
const { getCommonResEffect } = publicStore()
const highlightjs = highlightjsP.component
const sourceM = useMachine(machine)
const code = ref('')
const locationMap = reactive(new Map<string, string[]>())
const getSource = (data: { classItem: Item; }) => {
sourceM.send({
type: "SUBMIT",
value: {
action: "exec",
command: `jad ${data.classItem.value}`
}
})
}
getCommonResEffect(sourceM, body => {
const result = body.results[0]
if (result.type === "jad") {
code.value = result.source
locationMap.clear()
locationMap.set('location', [result.location])
Object.entries(result.classInfo).forEach(([k, v]) => {
let value: string[] = []
if (k === "classloader") value = v as string[]
else value.push(v.toString())
locationMap.set(k, value)
})
}
})
onBeforeMount(() => {
sourceM.send("INIT")
})
</script>
<template>
<!-- <div class="flex flex-col items-center w-full"> -->
<div class="mb-4">
<ClassInput :submit-f="getSource"></ClassInput>
</div>
<div v-if="code !== ''">
<CmdResMenu title="classInfo" :map="locationMap" class="mb-4"></CmdResMenu>
<div class="w-10/12 rounded-xl border p-4 bg-[#f6f6f6] hover:shadow-gray-400 mx-auto shadow-lg transition mb-4">
<highlightjs language="Java" :code="code" />
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,148 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import { useMachine } from '@xstate/vue';
import { onBeforeMount, reactive, ref } from 'vue';
import AutoComplete from "@/components/input/AutoComplete.vue";
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { waitFor } from 'xstate/lib/waitFor';
import { interpret } from 'xstate';
import permachine from '@/machines/perRequestMachine';
const { getCommonResEffect } = publicStore()
const searchMbean = useMachine(machine)
const allMbean = useMachine(machine)
const optionItems = ref([] as { name: string, value: string }[])
const attributesMap = reactive(new Map<string, string[]>())
const constructorsMap = reactive(new Map<string, string[]>())
const operationMap = reactive(new Map<string, string[]>())
const className = ref('')
const description = ref('')
let mbeanName = ''
onBeforeMount(() => {
searchMbean.send("INIT")
allMbean.send("INIT")
// allLoop.open()
})
getCommonResEffect(searchMbean, body => {
optionItems.value.length = 0
const result = body.results[0]
if (result.type === "mbean") {
if (Object.hasOwn(result, "mbeanMetadata") && Object.hasOwn(result.mbeanMetadata, mbeanName)) {
const res = result.mbeanMetadata[mbeanName]
attributesMap.clear()
constructorsMap.clear()
operationMap.clear()
className.value = ''
description.value = ''
res.attributes
.forEach(v => {
attributesMap.set(
v.name,
Object
.entries(v)
.filter(([k, v]) => k !== "name")
.map(([k, v]) => {
if (k === "openType") return `${k} : ${JSON.stringify(v)}`
return `${k} : ${v.toString()}`
})
)
})
res.constructors
.forEach(v => {
constructorsMap.set(
v.name,
Object
.entries(v)
.filter(([k, v]) => k !== "name")
.map(([k, v]) => {
if (k === "signature") return `${k} : ${JSON.stringify(v)}`
return `${k} : ${v.toString()}`
})
)
})
res.operations
.forEach(operation => {
operationMap.set(
operation.name,
Object
.entries(operation)
.filter(([k, v]) => k !== "name")
.map(([k, v]) => {
if (k === "signature") return `${k} : ${JSON.stringify(v)}`
return `${k} : ${v.toString()}`
})
)
})
className.value = res.className
description.value = res.description
}
if (Object.hasOwn(result, "mbeanAttribute") && Object.hasOwn(result.mbeanAttribute, mbeanName)) {
const res = result.mbeanAttribute[mbeanName]
res.forEach(({ name, value }) => {
let format = "value : "
if (["string", "number", "boolean"].includes(typeof value)) {
format += value.toString()
} else {
format += JSON.stringify(value)
}
const v = attributesMap.get(name)
if (v) v.push(format)
else attributesMap.set(name, [format])
})
}
}
})
const getMbeanInfo = async (item: Item) => {
searchMbean.send({
type: "SUBMIT",
value: {
action: "exec",
command: `mbean -m ${item.value}`
}
})
mbeanName = item.value as string
await waitFor(searchMbean.service, state => state.matches("ready"))
searchMbean.send({
type: "SUBMIT",
value: {
action: "exec",
command: `mbean ${item.value}`
}
})
}
const getAll = () => fetchStore().baseSubmit(interpret(permachine), {
action: "exec",
command: `mbean`
}).then(res=>{
const result = (res as CommonRes).body.results[0]
optionItems.value.length = 0
if (result.type === "mbean" && Object.hasOwn(result, "mbeanNames")) {
result.mbeanNames.forEach(name => {
optionItems.value.push({
name,
value: name
})
})
}
})
</script>
<template>
<AutoComplete label="mbeanInfo" :option-items="optionItems" :input-fn="getAll" v-slot="slotP" as="form">
<button @click.prevent="getMbeanInfo(slotP.selectItem)"
class="btn btn-primary btn-sm btn-outline mx-2 transition">submit</button>
</AutoComplete>
<div v-if="className !== ''" class="mt-4">
<h2 class="flex justify-center my-4 text-xl">{{ className }}</h2>
<div class="flex my-4 pl-10">description : {{ description }}</div>
<CmdResMenu title="arrtibute" :map="attributesMap"></CmdResMenu>
<CmdResMenu title="constructors" :map="constructorsMap"></CmdResMenu>
<CmdResMenu title="operations" :map="operationMap"></CmdResMenu>
</div>
</template>
@@ -0,0 +1,55 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { useMachine } from '@xstate/vue';
import { onBeforeMount, onUnmounted, reactive, watchEffect } from 'vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { publicStore } from '@/stores/public';
import { fetchStore } from '@/stores/fetch';
import { number } from 'echarts/core';
const fetchM = useMachine(machine)
const { getPollingLoop } = fetchStore()
const { getCommonResEffect } = publicStore()
const map = reactive(new Map<string, string[]>())
const loop = getPollingLoop(() => {
fetchM.send({
type: "SUBMIT",
value: {
action: "exec",
command: "memory"
}
})
})
onBeforeMount(() => {
fetchM.send("INIT")
loop.open()
})
onUnmounted(()=>loop.close())
getCommonResEffect(fetchM, body => {
const result = body.results[0]
if (result.type === "memory") {
const memoryInfo = result.memoryInfo
map.clear()
Object.entries(memoryInfo).reduce((pre, cur) => {
cur[1].forEach(v => pre.push(v))
return pre
}, [] as any[]).forEach(v => {
map.set(v.name,
Object.entries(v).filter(([k, v]) => k !== "name").map((k) => {
return `${k[0]} : ${typeof k[1] === "number" && k[1] > 0 ?
(Math.floor(k[1] / 1024 / 1024) + 'M'):k[1] }`
})
)
})
}
})
</script>
<template>
<CmdResMenu title="memory" :map="map" class="w-full" />
</template>
<style scoped>
</style>
@@ -0,0 +1,84 @@
<script setup lang="ts">
import 'highlight.js/lib/common';
import highlightjsP from "@highlightjs/vue-plugin";
import { useInterpret } from '@xstate/vue';
import { ref } from 'vue';
import { fetchStore } from '@/stores/fetch';
import permachine from '@/machines/perRequestMachine';
import { publicStore } from '@/stores/public';
const publiC = publicStore()
const express = ref("")
const highlightjs = highlightjsP.component
const sourceM = useInterpret(permachine)
const code = ref('')
const depth = ref(1)
const classloaderName = ref("")
const hashcode = ref("")
const setDepth = publiC.inputDialogFactory(
depth,
(raw) => {
const valRaw = parseInt(raw)
const realVal = Number.isNaN(valRaw) ? 1 : valRaw
return realVal
},
(input) => input.value.toString(),
)
const setHash = publiC.inputDialogFactory(
hashcode,
(raw) => raw,
(input) => input.value.toString(),
)
const setClassLoader = publiC.inputDialogFactory(
classloaderName,
(raw) => raw,
(input) => input.value.toString(),
)
const getSource = () => {
let nhash = ""
let nclassLoader = ""
let ndepth = `-x ${depth.value}`
if(classloaderName.value !== "") nclassLoader += `--classLoaderClass ${classloaderName.value}`
if(hashcode.value !== "") nhash += `-c ${hashcode.value}}`
fetchStore().baseSubmit(sourceM, {
action: "exec",
command: `ognl ${express.value} ${nclassLoader} ${nhash} ${ndepth}`
}).then(
res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "ognl") {
code.value = result.value
}
}
)
}
</script>
<template>
<form class="mb-4 flex items-center justify-between">
<label class="flex flex-1 items-center"> ognl
<div class="w-full cursor-default
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline
outline-2
min-w-[15rem]
mx-2
hover:shadow-md transition">
<input type="text" v-model="express"
class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none">
</div>
<button class="btn btn-sm btn-outline mr-2" @click.prevent="setDepth">depth:{{depth}}</button>
<button class="btn btn-sm btn-outline mr-2" @click.prevent="setClassLoader" v-if="hashcode === ''">ClassLoaderClass:{{classloaderName}}</button>
<button class="btn btn-sm btn-outline mr-2" @click.prevent="setHash" v-if="classloaderName === ''">hashcode:{{hashcode}}</button>
</label>
<button @click.prevent="getSource"
class="btn btn-primary btn-sm btn-outline truncate p-2">submit</button>
</form>
<div v-if="code !== ''">
<!-- <CmdResMenu title="classInfo" :map="locationMap" class="mb-4"></CmdResMenu> -->
<div class="w-10/12 rounded-xl border p-4 bg-[#f6f6f6] hover:shadow-gray-400 mx-auto shadow-lg transition mb-4">
<highlightjs language="bash" :code="code" />
</div>
</div>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
/**
* @zh reset 功能比较常用,之后应该装载到header上进行操作
*/
import ClassInput from '@/components/input/ClassInput.vue';
import { reactive } from 'vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { fetchStore } from '@/stores/fetch';
import { interpret } from 'xstate';
import permachine from '@/machines/perRequestMachine';
const fetchS = fetchStore()
const res = reactive(new Map())
const resetClass = (data: { classItem: Item }) => {
fetchS.baseSubmit(interpret(permachine), {
action: "exec",
command: `reset ${data.classItem.value as string}`
}).then(response => {
const result = (response as CommonRes).body.results[0]
if (result.type === "reset") {
Object.entries(result.affect).forEach(([k, v]) => {
res.set(k, k === "cost" ? [`${v}ms`] : [v])
})
}
})
}
</script>
<template>
<ClassInput :submit-f="resetClass"></ClassInput>
<CmdResMenu title="reset affect" open :map="res" v-if="res.size > 0"></CmdResMenu>
</template>
@@ -0,0 +1,92 @@
<script setup lang="ts">
import machine from '@/machines/consoleMachine';
import { useInterpret } from '@xstate/vue';
import { reactive, ref } from 'vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import { Switch, } from '@headlessui/vue';
import { interpret } from 'xstate';
import permachine from '@/machines/perRequestMachine';
const retransformListM = useInterpret(machine)
const { getPollingLoop } = fetchStore()
const retransformListMap = reactive(new Map<string, string[]>())
const fetchS = fetchStore()
const retransformRes = reactive(new Map<string, string[]>())
const retransformPath = ref('')
const enabled = ref(false)
const listLoop = getPollingLoop(() => {
fetchS.baseSubmit(retransformListM, {
action: "exec",
command: "retransform -l"
}).then(res => {
const result = (res as CommonRes).body.results[0]
retransformListMap.clear()
if (result.type === "retransform") {
result.retransformEntries.forEach(v => {
console.log(111)
retransformListMap.set(v.className, Object.entries(v).filter(([k, v]) => k !== "className").map(([k, v]) => `${k} : ${v.toString()}`))
})
}
})
}, {
step: 2000,
globalIntrupt: true
})
const onSubmit = () => {
let classPattern = ""
if (enabled.value) classPattern += `--classPattern`
return fetchStore().baseSubmit(interpret(permachine), {
action: "exec",
command: `retransform ${retransformPath.value} ${classPattern}`
}).then(res => {
let result = (res as CommonRes).body.results[0]
if (result.type === "retransform") {
retransformRes.clear()
retransformRes.set("retransformClass", result.retransformClasses)
retransformRes.set("retransformCount", [result.retransformCount.toString()])
}
})
}
const openList = () => {
listLoop.invoke()
}
</script>
<template>
<form class=" flex items-center justify-between mb-2">
<label class="flex flex-1 items-center"> retransform
<div class="w-full cursor-default
overflow-hidden rounded-lg bg-white text-left border
focus-within:outline
outline-2
min-w-[15rem]
mx-2
hover:shadow-md transition">
<input type="text" v-model="retransformPath"
class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus-visible:outline-none">
</div>
</label>
<!-- <div class="flex input-btn-style mr-2 focus-within:outline outline-2">
<div class="mx-2">explicitly trigger</div>
<Switch v-model="enabled" :class="enabled ? 'bg-blue-400' : 'bg-gray-500'"
class="relative items-center inline-flex h-6 w-12 shrink-0 cursor-pointer rounded-full border-transparent transition-colors ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 mr-2">
<span aria-hidden="true" :class="enabled ? 'translate-x-6' : '-translate-x-1'"
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow-md shadow-gray-500 ring-0 transition ease-in-out" />
</Switch>
</div> -->
<label class="label cursor-pointer btn-sm border border-neutral mr-2">
<span class="label-text uppercase font-bold mr-1">explicitly trigger</span>
<input v-model="enabled" type="checkbox" class="toggle" />
</label>
<button @click.prevent="onSubmit" class="btn btn-primary btn-sm btn-outline mr-4 truncate">submit</button>
</form>
<CmdResMenu title="response" :map="retransformRes" open v-if="retransformRes.size !== 0"></CmdResMenu>
<CmdResMenu title="entries" :map="retransformListMap" @myclick="openList"></CmdResMenu>
</template>
@@ -0,0 +1,292 @@
<script setup lang="ts">
// import machine from '@/machines/consoleMachine';
import { useInterpret, } from '@xstate/vue';
import { onBeforeMount, reactive, ref, onUnmounted, watchEffect } from 'vue';
import CmdResMenu from '@/components/show/CmdResMenu.vue';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import TodoList from '@/components/input/TodoList.vue';
import {
Switch, SwitchLabel, SwitchGroup,
Listbox, ListboxButton, ListboxOptions, ListboxOption
} from '@headlessui/vue';
import { transfromStore } from "@/stores/resTransform"
import permachine from '@/machines/perRequestMachine';
import { computed } from '@vue/reactivity';
const fetchS = fetchStore()
const FetchService = useInterpret(permachine)
const stackTrace = reactive([] as string[])
const transformS = transfromStore()
const count = ref(0)
const leastTime = ref(200)
const isBlock = ref(false)
const publiC = publicStore()
type thkey = keyof BusyThread
const keyList: thkey[] = [
"id",
"name",
"cpu",
"daemon",
"deltaTime",
"group",
"interrupted",
"priority",
"state",
"time",
"inNative",
"suspended",
"waitedCount",
"waitedTime",
"lockOwnerId",
"lockedMonitors",
"lockedSynchronizers",
"blockedCount",
"blockedTime",
]
const statsList: (keyof ThreadStats)[] = ["id",
"name",
"cpu",
"daemon",
"deltaTime",
"group",
"interrupted",
"priority",
"state",
"time"]
const infoCount = ref({
NEW: 0,
RUNNABLE: 0,
BLOCKED: 0,
WAITING: 0,
TIMED_WAITING: 0,
TERMINATED: 0
} as ThreadStateCount)
const tableResults = reactive([] as Map<string, string>[])
const tableFilter = computed(() => {
// 原本的数组
let res = tableResults
if (includesVal.size === 0) return res;
// 导入过滤条件
includesVal.forEach((v1) => {
let [key, vals] = v1.split(":")
//@ts-ignore
if (keyList.includes(key)) {
const raw = vals.split("")
let incudes: string[] = []
if (raw[0] === "[" && raw[raw.length - 1] === "]") {
raw.pop()
raw.shift()
}
incudes = raw.join("").split(',')
if (key === "name" || key === "group") {
// 字符串是包含
res = res.filter((map) => {
let val = map.get(key.trim())!
// 取对于多个子字符串, 取交集
return incudes.every(reg => val.includes(reg))
})
} else res = res.filter((map) => incudes.includes(map.get(key.trim())!))
}
})
return res
})
const statelist: { name: string, value: ThreadState | "" }[] = [
{ name: "WAITING", value: "WAITING" },
{ name: "RUNNABLE", value: "RUNNABLE" },
{ name: "TIMED_WAITING", value: "TIMED_WAITING" },
{ name: "BLOCKED", value: "BLOCKED" },
{ name: "all", value: "" }
]
const threadState = ref(statelist[4])
const includesVal = reactive(new Set<string>())
onBeforeMount(() => {
getThreads()
})
const getThreads = () => {
let i = leastTime.value > 0 ? "-i " + leastTime.value : ""
let n = count.value > 0 ? "-n " + count.value : ""
const b = isBlock.value ? "-b" : ""
let state = threadState.value.value === "" ? "" : `--state ${threadState.value.value}`
tableResults.length = 0
for (const key in infoCount.value) {
//@ts-ignore
infoCount.value[key] = 0
}
// thread [--all] [-b] [--lockedMonitors] [--lockedSynchronizers] [-i <value>] [--state <value>] [-n <value>] [id]
fetchS.baseSubmit(FetchService, {
action: "exec",
command: `thread --all ${b} ${i} ${n} ${state}`
}).then(res => {
const result = (res as CommonRes).body.results[0]
// threadInfo.value = result.threadInfo
if (result.type === "thread") {
stackTrace.length = 0
if (n === "") {
// result.threadStateCount.
result.threadStats.forEach(thread => {
const map = new Map()
// Object.entries(thread).forEach(([k, v]) => map.set(k, v.toString().trim() || "-"))
for (const key in thread) {
// if(key === 'id') map.set(key, thread[key as thkey])
map.set(key, thread[key as keyof ThreadStats].toString().trim() || "-")
}
tableResults.unshift(map)
})
for (const key in result.threadStateCount) {
if (Object.hasOwn(result.threadStateCount, key)) {
//@ts-ignore
infoCount.value[key] = result.threadStateCount[key];
}
}
} else {
result.busyThreads.forEach((thread) => {
// allMap.set(v.name, Object.entries(v).filter(([k, v]) => k !== "name").map(([k, v]) => `${k} : ${v}`))
// if (v.id > 0) optionThread.push({ name: v.name, value: v.id })
const map = new Map()
// Object.entries(thread).forEach(([k, v]) => map.set(k, v.toString().trim() || "-"))
for (const key in thread) {
// if(key === 'id') map.set(key, thread[key as thkey])
map.set(key, thread[key as thkey].toString().trim() || "-")
}
tableResults.unshift(map)
})
}
tableResults.sort((m1, m2) => parseFloat(m2.get("cpu")!) - parseFloat(m1.get("cpu")!))
}
})
}
const setlimit = publiC.inputDialogFactory(
count,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 3 : valRaw
},
(input) => input.value.toString(),
)
const setleast = publiC.inputDialogFactory(
leastTime,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 200 : valRaw
},
(input) => input.value.toString(),
)
const getSpecialThreads = (threadid: number = -1) => {
let threadName = threadid > 0 ? `${threadid}` : ""
// let i = leastTime.value > 0 ? "-i " + leastTime.value : ""
// let n = count.value > 0 ? "-n " + count.value : ""
// const b = isBlock.value ? "-b" : ""
// let state = threadState.value.value === "" ? "" : `--state ${threadState.value.value}`
fetchS.baseSubmit(FetchService, {
action: "exec",
command: `thread ${threadName}`
}).then(res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "thread") {
// threadInfo.value = result.threadInfo
stackTrace.length = 0
result.threadInfo.stackTrace.forEach(stack => stackTrace.unshift(transformS.transformStackTrace(stack)))
}
})
}
</script>
<template>
<div class="flex flex-col h-full">
<div class="flex justify-end items-center h-[10vh]">
<TodoList title="filter" :val-set="includesVal" class=" mr-2"></TodoList>
<label class="label cursor-pointer btn-sm border border-neutral ">
<span class="label-text uppercase font-bold mr-1">is blocking:</span>
<input v-model="isBlock" type="checkbox" class="toggle"/>
</label>
<button class="btn ml-2 btn-sm btn-outline" @click="setleast">sample interval:{{leastTime}}</button>
<button v-show="!isBlock" class="btn ml-2 btn-sm btn-outline" @click="setlimit"> top n threads:{{count}}</button>
<Listbox v-model="threadState">
<div class=" relative mx-2 ">
<ListboxButton class="btn w-40 btn-sm btn-outline">state {{ threadState.name }}</ListboxButton>
<ListboxOptions
class=" z-10 absolute w-40 mt-2 border overflow-hidden rounded-md hover:shadow-xl transition bg-base-100">
<ListboxOption v-for="(am,i) in statelist" :key="i" :value="am" v-slot="{active, selected}">
<div class=" p-2 transition " :class="{
'bg-neutral text-neutral-content': active,
'bg-neutral-focus text-neutral-content': selected,
}">
{{ am.name }}
</div>
</ListboxOption>
</ListboxOptions>
</div>
</Listbox>
<button class="btn btn-primary btn-sm btn-outline" @click="getThreads"> get threads</button>
</div>
<div class="w-full h-[50vh] input-btn-style my-2 p-4 flex flex-col">
<div class="flex h-[8vh] flex-wrap flex-auto">
<div v-for="(v, i) in Object.entries(infoCount)" :key="i" class="mr-2">
<span class="text-primary-content border border-primary-focus bg-primary-focus w-44 px-2 rounded-l">
{{ v[0] }}
</span>
<span class="border border-primary-focus bg-base-200 rounded-r flex-1 px-1">
{{v[1]}}
</span>
</div>
</div>
<div class="overflow-auto h-[40vh] w-full">
<table class="table w-full group">
<thead>
<tr>
<th class="border border-slate-300 p-2 group-first:z-0">get stackTrace</th>
<template v-if="count===0">
<th class="border border-slate-300 p-2" v-for="(v,i) in keyList" :key="i">{{v}}</th>
</template>
<template v-else>
<th class="border border-slate-300 p-2" v-for="(v,i) in statsList" :key="i">{{v}}</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="(map, i) in tableFilter" :key="i">
<td class="border border-slate-300 p-2"><button class="btn-outline btn-primary btn btn-sm"
@click="getSpecialThreads(parseInt(map.get('id')!))" v-if="map.get('id')!=='-1'">
get stackTrace
</button></td>
<template v-if="count === 0">
<td class="border border-slate-300 p-2" v-for="(key,j) in keyList" :key="j">
{{map.get(key)}}
</td>
</template>
<template v-else>
<td class="border border-slate-300 p-2" v-for="(key,j) in statsList" :key="j">
{{map.get(key)}}
</td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<div class="input-btn-style flex-auto overflow-auto">
<h2 class="text-lg">stackTrace</h2>
<div v-for="(stack, i) in stackTrace" class="mb-2" :key="i">{{stack}} </div>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,110 @@
<script setup lang="ts">
import ClassInput from '@/components/input/ClassInput.vue';
import Tree from '@/components/show/Tree.vue';
import permachine from '@/machines/perRequestMachine';
import { fetchStore } from '@/stores/fetch';
import { publicStore } from '@/stores/public';
import { useInterpret } from '@xstate/vue';
import { reactive, ref } from 'vue';
const pollResults = reactive<TreeNode[]>([])
const gcMachine = useInterpret(permachine)
const fetchS = fetchStore()
const publicS = publicStore()
const depth = ref(1)
const setDepth = publicS.inputDialogFactory(
depth,
(raw) => {
let valRaw = parseInt(raw)
return Number.isNaN(valRaw) ? 1 : valRaw
},
(input) => input.value.toString(),
)
const getInstance = (data:{classItem:Item}) => {
pollResults.length = 0
fetchS.baseSubmit(gcMachine, {
action: "exec",
command: `vmtool --action getInstances --className ${data.classItem.value} -x ${depth.value}`
}).then(
res => {
const result = (res as CommonRes).body.results[0]
if (result.type === "vmtool") {
let raw = result.value.split("\n")
const stk: TreeNode[] = []
// Tree的构建
raw.forEach(v => {
let str = v.trim()
let match = 0
for (let s of str) {
if (s === "[") {
match++
} else if (s === "]") {
match--
}
}
const root = {
children: [],
meta: str.substring(0, str.length - 1)
} as TreeNode
if (match > 0) {
stk.push(root)
} else if (match === 0) {
let cur = stk.pop()
if (cur) {
cur.children!.push(root)
stk.push(cur)
} else {
stk.push(root)
}
} else {
/// 默认每行只会一个]
//!可能会有bug
let cur = stk.pop()!
if (stk.length > 0) {
let parent = stk.pop()!
parent.children!.push(cur)
stk.push(parent)
} else {
// 构建结束
stk.push(cur)
}
}
console.log(JSON.stringify(stk))
})
pollResults.unshift(stk[0])
}
}
)
}
</script>
<template>
<ClassInput :submit-f="getInstance" class="mb-4" >
<template #others>
<button class="ml-2 btn btn-outline btn-sm" @click="setDepth">depth:{{depth}}</button>
</template>
</ClassInput>
<template v-if="pollResults.length > 0">
<ul class=" pointer-events-auto mt-10">
<template v-for="(result, i) in pollResults" :key="i">
<Tree :root="result" class="mt-2">
<template #meta="{ data }">
<div class="bg-info p-1 mb-1 rounded-r rounded-br">
{{data}}
</div>
</template>
</Tree>
</template>
</ul>
</template>
</template>
<style scoped>
</style>
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import * as path from "path";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
reactivityTransform: path.resolve(__dirname, "./ui"),
})],
// root:path.resolve(__dirname,"src"),
resolve: {
alias: {
"@": path.resolve(__dirname, "./ui/src/")
},
},
build: {
emptyOutDir:true,
rollupOptions: {
input: {
main: path.resolve(__dirname, "index.html"),
ui: path.resolve(__dirname, "ui/index.html"),
},
output:{
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames:'static/js/[name]-[hash].js',
assetFileNames:'static/[ext]/[name]-[hash].[ext]'
}
},
},
base: "/",
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:8563",
changeOrigin: true,
},
},
},
});
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>arthas-all</artifactId>
<groupId>com.taobao.arthas</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>web-ui</artifactId>
<packaging>pom</packaging>
<name>web-ui</name>
<!-- FIXME change it to the project's website -->
<!-- <url>http://www.example.com</url>-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<yarn.registry.url>https://registry.npmmirror.com/</yarn.registry.url>
<yarn.download.url>http://npmmirror.com</yarn.download.url>
<node.download.url>https://npmmirror.com/mirrors/node/</node.download.url>
<node.version>v16.16.0</node.version>
<yarn.version>v1.22.19</yarn.version>
</properties>
<build>
<finalName>web-ui</finalName>
<!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<!-- Use the latest released version:
https://repo1.maven.org/maven2/com/github/eirslett/frontend-maven-plugin/ -->
<version>1.12.1</version>
<executions>
<execution>
<!-- optional: you don't really need execution ids, but it looks nice in your build log. -->
<id>install node and yarn</id>
<goals>
<goal>install-node-and-yarn</goal>
</goals>
<!-- optional: default phase is "generate-resources" -->
<phase>generate-resources</phase>
</execution>
<execution>
<id>set registry</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>config set registry ${yarn.registry.url}</arguments>
</configuration>
</execution>
<execution>
<id>yarn install</id>
<goals>
<goal>yarn</goal>
</goals>
<configuration>
<!-- optional: The default argument is actually
"install", so unless you need to run some other yarn command,
you can remove this whole <configuration> section.
-->
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>vite build</id>
<goals>
<goal>yarn</goal>
</goals>
<configuration>
<arguments>vite build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<yarnVersion>${yarn.version}</yarnVersion>
<!-- optional: where to download node from. Defaults to https://nodejs.org/dist/ -->
<nodeDownloadRoot>${node.download.url}</nodeDownloadRoot>
<!-- optional: where to download yarn from. Defaults to https://github.com/yarnpkg/yarn/releases/download/ -->
<!-- <yarnDownloadRoot>${yarn.registry.url}</yarnDownloadRoot>-->
<workingDirectory>arthasWebConsole</workingDirectory>
<installDirectory>target</installDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy dist</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>arthasWebConsole/dist</directory>
</resource>
</resources>
<outputDirectory>${project.build.directory}/static</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- <plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin> -->
</plugins>
</build>
</project>