Skip to content

Commit

Permalink
feat(status): setup websocket server
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Mar 7, 2021
1 parent 9dcdd69 commit 10e9bf6
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 152 deletions.
2 changes: 1 addition & 1 deletion build/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const KOISHI_VERSION = JSON.stringify(version)
entryPoints.push(base + '/src/worker.ts')
} else if (name === 'koishi-test-utils') {
await tasks[chai]
} else if (name === 'plugin-webui') {
} else if (name === 'plugin-status') {
entryPoints.splice(0, 1, base + '/server/index.ts')
}

Expand Down
18 changes: 8 additions & 10 deletions packages/plugin-status/client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@ interface Status {
const status = ref<Status>(null)
if (import.meta.hot) {
import.meta.hot.on('update', (data) => {
console.log('update', data)
})
}
onMounted(async () => {
const res = await fetch(KOISHI_SERVER + '/_')
const data = await res.json()
status.value = data
console.log('fetch', data)
const socket = new WebSocket(KOISHI_ENDPOINT)
socket.onmessage = (ev) => {
const data = JSON.parse(ev.data)
console.log('receive', data)
if (data.type === 'update') {
status.value = data.body
}
}
})
</script>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-status/client/components/load-bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default {
height: 100%;
position: relative;
float: left;
transition: 0.3s ease;
transition: 0.6s ease;
};
> *:hover {
z-index: 10;
Expand Down
5 changes: 4 additions & 1 deletion packages/plugin-status/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@
"koishi-test-utils": "^6.0.0-beta.10"
},
"dependencies": {
"@types/ws": "^7.4.0",
"@vitejs/plugin-vue": "^1.1.5",
"@vue/compiler-sfc": "^3.0.7",
"element-plus": "^1.0.2-beta.33",
"sass": "^1.32.8",
"systeminformation": "^5.6.1",
"vite": "^2.0.5",
"vue": "^3.0.7"
"vue": "^3.0.7",
"ws": "^7.4.3"
}
}
133 changes: 15 additions & 118 deletions packages/plugin-status/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Context, App, Argv, Bot, Platform } from 'koishi-core'
import { cpus, totalmem, freemem } from 'os'
import { Context, App, Argv } from 'koishi-core'
import { interpolate, Time } from 'koishi-utils'
import { ActiveData } from './database'
import * as WebUI from './webui'
import Profile from './profile'

export * from './database'

Expand All @@ -10,87 +11,20 @@ declare module 'koishi-core' {
counter: number[]
}

interface App {
startTime: number
getStatus(): Promise<BaseStatus>
interface EventMap {
'status/tick'(): void
}
}

App.prototype.getStatus = async function (this: App) {
const bots = await Promise.all(this.bots.map(async (bot): Promise<BotStatus> => ({
platform: bot.platform,
selfId: bot.selfId,
username: bot.username,
code: await bot.getStatus(),
rate: bot.counter.slice(1).reduce((prev, curr) => prev + curr, 0),
})))
const memory = memoryRate()
const cpu: Rate = [appRate, usedRate]
return { bots, memory, cpu, startTime: this.startTime }
}

export interface Config {
path?: string
export interface Config extends WebUI.Config {
refresh?: number
format?: string
formatBot?: string
}

let usage = getCpuUsage()
let appRate: number
let usedRate: number

function memoryRate(): Rate {
const totalMemory = totalmem()
return [process.memoryUsage().rss / totalMemory, 1 - freemem() / totalMemory]
}

function getCpuUsage() {
let totalIdle = 0, totalTick = 0
const cpuInfo = cpus()
const usage = process.cpuUsage().user

for (const cpu of cpuInfo) {
for (const type in cpu.times) {
totalTick += cpu.times[type]
}
totalIdle += cpu.times.idle
}

return {
app: usage / 1000,
used: (totalTick - totalIdle) / cpuInfo.length,
total: totalTick / cpuInfo.length,
}
}

function updateCpuUsage() {
const newUsage = getCpuUsage()
const totalDifference = newUsage.total - usage.total
appRate = (newUsage.app - usage.app) / totalDifference
usedRate = (newUsage.used - usage.used) / totalDifference
usage = newUsage
}

export type Rate = [app: number, total: number]

export interface BaseStatus {
bots: BotStatus[]
memory: Rate
cpu: Rate
startTime: number
}

export interface Status extends BaseStatus, ActiveData {
export interface Status extends Profile, ActiveData {
timestamp: number
}

export interface BotStatus {
username?: string
selfId: string
platform: Platform
code: Bot.Status
rate?: number
startTime: number
}

type StatusCallback = (this: App, status: Status, config: Config) => void | Promise<void>
Expand All @@ -106,7 +40,6 @@ extend(async function (status) {
})

const defaultConfig: Config = {
path: '/status',
refresh: Time.minute,
// eslint-disable-next-line no-template-curly-in-string
formatBot: '{{ username }}:{{ code ? `无法连接` : `工作中(${rate}/min)` }}',
Expand All @@ -115,59 +48,20 @@ const defaultConfig: Config = {
'==========',
'活跃用户数量:{{ activeUsers }}',
'活跃群数量:{{ activeGroups }}',
'启动时间:{{ new Date(startTime).toLocaleString("zh-CN", { hour12: false }) }}',
'CPU 使用率:{{ (cpu.app * 100).toFixed() }}% / {{ (cpu.total * 100).toFixed() }}%',
'内存使用率:{{ (memory.app * 100).toFixed() }}% / {{ (memory.total * 100).toFixed() }}%',
'CPU 使用率:{{ (cpu[0] * 100).toFixed() }}% / {{ (cpu[1] * 100).toFixed() }}%',
'内存使用率:{{ (memory[0] * 100).toFixed() }}% / {{ (memory[1] * 100).toFixed() }}%',
].join('\n'),
}

export const name = 'status'

export function apply(ctx: Context, config: Config = {}) {
const all = ctx.all()
const { refresh, formatBot, format } = { ...defaultConfig, ...config }

all.on('command', ({ session }: Argv<'lastCall'>) => {
ctx.all().on('command', ({ session }: Argv<'lastCall'>) => {
session.user.lastCall = new Date()
})

all.before('send', (session) => {
session.bot.counter[0] += 1
})

let timer: NodeJS.Timeout
ctx.on('connect', async () => {
ctx.app.startTime = Date.now()

ctx.bots.forEach((bot) => {
bot.counter = new Array(61).fill(0)
})

timer = setInterval(() => {
updateCpuUsage()
ctx.bots.forEach(({ counter }) => {
counter.unshift(0)
counter.splice(-1, 1)
})
}, 1000)

if (!ctx.router) return
ctx.router.get('/status', async (ctx) => {
const status = await getStatus().catch<Status>((error) => {
all.logger('status').warn(error)
return null
})
if (!status) return ctx.status = 500
ctx.set('Content-Type', 'application/json')
ctx.set('Access-Control-Allow-Origin', '*')
ctx.body = status
})
})

ctx.before('disconnect', () => {
clearInterval(timer)
})

ctx.command('status', '查看机器人运行状态')
.shortcut('你的状态', { prefix: true })
.shortcut('你的状况', { prefix: true })
Expand All @@ -190,7 +84,7 @@ export function apply(ctx: Context, config: Config = {}) {
})

async function _getStatus() {
const status = await ctx.app.getStatus() as Status
const status = await Profile.from(ctx) as Status
await Promise.all(callbacks.map(callback => callback.call(ctx.app, status, config)))
status.timestamp = timestamp
return status
Expand All @@ -205,4 +99,7 @@ export function apply(ctx: Context, config: Config = {}) {
timestamp = now
return cachedStatus = _getStatus()
}

ctx.plugin(Profile)
if (config.port) ctx.plugin(WebUI, config)
}
108 changes: 108 additions & 0 deletions packages/plugin-status/server/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Bot, Context, Platform } from 'koishi-core'
import { Time } from 'koishi-utils'
import { cpus } from 'os'
import { mem } from 'systeminformation'

export type Rate = [app: number, total: number]

let usage = getCpuUsage()
let appRate: number
let usedRate: number

async function memoryRate(): Promise<Rate> {
const { total, active } = await mem()
return [process.memoryUsage().rss / total, active / total]
}

function getCpuUsage() {
let totalIdle = 0, totalTick = 0
const cpuInfo = cpus()
const usage = process.cpuUsage().user

for (const cpu of cpuInfo) {
for (const type in cpu.times) {
totalTick += cpu.times[type]
}
totalIdle += cpu.times.idle
}

return {
// microsecond values
app: usage / 1000,
// use total value (do not know how to get the cpu on which the koishi is running)
used: (totalTick - totalIdle) / cpuInfo.length,
total: totalTick / cpuInfo.length,
}
}

function updateCpuUsage() {
const newUsage = getCpuUsage()
const totalDifference = newUsage.total - usage.total
appRate = (newUsage.app - usage.app) / totalDifference
usedRate = (newUsage.used - usage.used) / totalDifference
usage = newUsage
}

export interface BotData {
username?: string
selfId: string
platform: Platform
code: Bot.Status
rate?: number
}

export namespace BotData {
export const from = async (bot: Bot) => ({
platform: bot.platform,
selfId: bot.selfId,
username: bot.username,
code: await bot.getStatus(),
rate: bot.counter.slice(1).reduce((prev, curr) => prev + curr, 0),
} as BotData)
}

export interface Profile {
bots: BotData[]
memory: Rate
cpu: Rate
}

export namespace Profile {
export interface Config {
tick?: number
}

export async function from(ctx: Context) {
const [memory, bots] = await Promise.all([
memoryRate(),
Promise.all(ctx.bots.map(BotData.from)),
])
const cpu: Rate = [appRate, usedRate]
return { bots, memory, cpu } as Profile
}

export function apply(ctx: Context, config: Config = {}) {
const { tick = 5 * Time.second } = config

ctx.all().before('send', (session) => {
session.bot.counter[0] += 1
})

ctx.on('connect', async () => {
ctx.bots.forEach((bot) => {
bot.counter = new Array(61).fill(0)
})

ctx.setInterval(() => {
updateCpuUsage()
ctx.bots.forEach(({ counter }) => {
counter.unshift(0)
counter.splice(-1, 1)
})
ctx.emit('status/tick')
}, tick)
})
}
}

export default Profile
Loading

0 comments on commit 10e9bf6

Please sign in to comment.