admin/monitoring: persistent sent/got
Massimo Melina committed
May 22, 2024 at 00:47 UTC
c3fe0ef39f9a7e64690603de9d7a01a0e0d0b622
8 files changed
+78
-42
admin/src/MonitorPage.ts
+31
-19
@@ -6,14 +6,17 @@ import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6
import { LinkOff, Lock, FolderZip, Upload, Download, ChevronRight, ChevronLeft } from '@mui/icons-material'
7
import { Box, Chip, ChipProps } from '@mui/material'
8
import { DataTable } from './DataTable'
9
-import { formatBytes, ipForUrl, CFG, formatSpeed, with_, createDurationFormatter, formatTimestamp,
10
- formatPerc, md } from "./misc"
9
+import {
10
+ formatBytes, ipForUrl, CFG, formatSpeed, with_, createDurationFormatter, formatTimestamp, formatPerc, md, Callback,
11
+ reactJoin,
12
+} from "./misc"
13
import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country, hTooltip } from './mui'
14
import { Field, SelectField } from '@hfs/mui-grid-form'
15
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
16
import { agentIcons } from './LogsPage'
17
import { state, useSnapState } from './state'
18
import { useBlockIp } from './useBlockIp'
19
+import { alertDialog } from './dialog'
20
21
export default function MonitorPage() {
22
return h(Fragment, {},
@@ -28,36 +31,45 @@ function MoreInfo() {
31
if (status && connections)
32
Object.assign(status, connections)
33
const [allInfo, setAllInfo] = useState(false)
31
- const xl = useBreakpoint('xl')
34
+ const lg = useBreakpoint('lg')
35
const md = useBreakpoint('md')
36
const sm = useBreakpoint('sm')
37
const formatDuration = createDurationFormatter({ maxTokens: 2, skipZeroes: true })
35
- return element || h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
38
+ return element || h(Box, { display: 'flex', flexWrap: 'wrap', gap: { xs: .5, md: 1 }, mb: { xs: 1, sm: 2 } },
39
(allInfo || md) && pair('started', {
40
label: "Uptime",
41
render: x => formatDuration(Date.now() - +new Date(x)),
42
title: x => "Started: " + formatTimestamp(x),
43
}),
41
- (allInfo || xl) && pair('http', { label: "HTTP", render: port }),
42
- (allInfo || xl) && pair('https', { label: "HTTPS", render: port }),
43
- (allInfo || sm) && pair('connections', { title: () => `${status.ips} IP(s)` }),
44
- pair('sent', { render: formatBytes, minWidth: '4em' }),
45
- (allInfo || sm) && pair('got', { render: formatBytes, minWidth: '4em' }),
46
- pair('outSpeed', { label: "Output speed", render: formatSpeedK, minWidth: '5em' }),
47
- (allInfo || md) && pair('inSpeed', { label: "Input speed", render: formatSpeedK, minWidth: '5em' }),
48
- !xl && h(IconBtn, { size: 'small', icon: allInfo ? ChevronLeft : ChevronRight, title: "Show more", onClick: () => setAllInfo(x => !x) }),
44
+ (allInfo || sm) && pair('sent_got', {
45
+ render: x => ({ Sent: formatBytes(x[0]), Got: formatBytes(x[1]) }),
46
+ onDelete: () => apiCall('clear_persistent', { k: ['totalSent', 'totalGot'] })
47
+ .then(() => alertDialog("Done", 'success'), alertDialog)
48
+ }),
49
+ (allInfo || sm) && pair('ips', { label: "IPs" }),
50
+ pair('outSpeed', { label: "Output", render: formatSpeedK, minWidth: '8.5em' }),
51
+ pair('inSpeed', { label: "Input", render: formatSpeedK, minWidth: '8.5em' }),
52
+ (md || allInfo && md || status?.http?.error) && pair('http', { label: "HTTP", render: port }),
53
+ (md || allInfo && md || status?.https?.error) && pair('https', { label: "HTTPS", render: port }),
54
+ !md && h(IconBtn, {
55
+ size: 'small',
56
+ icon: allInfo ? ChevronLeft : ChevronRight,
57
+ title: "Show more",
58
+ onClick: () => setAllInfo(x => !x)
59
+ }),
60
)
61
62
type Color = ChipProps['color']
52
- type Render = (v: any) => [string, Color?] | string
63
+ type Render = (v: any) => [string, Color?] | string | { [label: string]: string }
64
interface PairOptions {
65
label?: string
66
render?: Render
67
minWidth?: StandardCSSProperties['minWidth']
68
title?: (v: any) => string
69
+ onDelete?: Callback
70
}
71
60
- function pair(k: string, { label, minWidth, render, title }: PairOptions={}) {
72
+ function pair(k: string, { label, minWidth, render, title, onDelete }: PairOptions={}) {
73
let v = _.get(status, k)
74
if (v === undefined)
75
return null
@@ -73,11 +85,11 @@ function MoreInfo() {
85
return hTooltip(renderedTitle, undefined, h(Chip, {
86
variant: 'filled',
87
color,
76
- label: h(Fragment, {},
77
- h('b',{},label),
78
- ': ',
79
- h('span', { style:{ display: 'inline-block', minWidth } }, v),
80
- ),
88
+ onDelete,
89
+ label: reactJoin(' – ', _.map(_.isPlainObject(v) ? v : { [label]: v }, (v,label) =>
90
+ h('span', { style:{ display: 'inline-block', minWidth } },
91
+ h('b',{}, label), ': ', v,
92
+ ))),
93
}) )
94
}
95
src/api.monitor.ts
+10
-3
@@ -2,12 +2,13 @@
2
3
import _ from 'lodash'
4
import { Connection, getConnections } from './connections'
5
-import { shortenAgent, try_, wait } from './misc'
5
+import { apiAssertTypes, shortenAgent, try_, wait, wantArray } from './misc'
6
import { ApiHandlers } from './apiMiddleware'
7
import Koa from 'koa'
8
import { totalGot, totalInSpeed, totalOutSpeed, totalSent } from './throttler'
9
import { getCurrentUsername } from './auth'
10
import { SendListReadable } from './SendList'
11
+import { storedMap } from './persistence'
12
13
export default {
14
@@ -86,14 +87,20 @@ export default {
87
yield {
88
outSpeed: totalOutSpeed,
89
inSpeed: totalInSpeed,
89
- got: totalGot,
90
- sent: totalSent,
90
+ sent_got: [totalSent.get(), totalGot.get()],
91
connections: filtered.length,
92
ips: _.uniqBy(filtered, x => x.ip).length,
93
}
94
await wait(1000)
95
}
96
},
97
+
98
+ async clear_persistent({ k }) {
99
+ apiAssertTypes({ string_array: { k } })
100
+ for (const x of wantArray(k))
101
+ void storedMap.del(x)
102
+ },
103
+
104
} satisfies ApiHandlers
105
106
function ignore(conn: Connection) {
src/index.ts
+3
-1
@@ -26,6 +26,7 @@ import { geoFilter } from './geo'
26
import { rootsMiddleware } from './roots'
27
import events from './events'
28
import { trackIpsMw } from './ip'
29
+import { storedMap } from './persistence'
30
31
ok(_.intersection(Object.keys(frontEndApis), Object.keys(adminApis)).length === 0) // they share same endpoints, don't clash
32
@@ -50,7 +51,8 @@ app.use(sessionMiddleware)
51
.use(mount(API_URI, apiMiddleware({ ...frontEndApis, ...adminApis })))
52
.use(serveGuiAndSharedFiles)
53
.on('error', errorHandler)
53
-events.emit('app', app)
54
+storedMap.once('open', () =>
55
+ events.emit('app', app))
56
57
function errorHandler(err:Error & { code:string, path:string }) {
58
const { code } = err
src/log.ts
+1
-1
@@ -153,7 +153,7 @@ declare module "koa" {
153
}
154
}
155
156
-events.on('app', () => { // wait for app to be set
156
+events.once('app', () => { // wait for app to be set
157
app.context.logExtra = function(anything, params) { // no => as we need 'this'
158
_.merge((this as any).state, { logExtra: { ...anything, params } }) // params will be considered as parameters of the API
159
}
src/middlewares.ts
+1
-1
@@ -153,7 +153,7 @@ export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
153
// But koa-session doesn't support 2 cookies, so I made this hacky solution: keep track of the options object, to modify the key at run-time.
154
let internalSessionMw: any
155
let options: any
156
-events.on('app', () => // wait for app to be defined
156
+events.once('app', () => // wait for app to be defined
157
internalSessionMw = session(options = { signed: true, rolling: true, sameSite: 'lax' } as const, app) )
158
export const sessionMiddleware: Koa.Middleware = (ctx, next) => {
159
options.key = 'hfs_' + ctx.protocol
src/misc.ts
+3
-4
@@ -120,8 +120,7 @@ export class AsapStream<T> extends Readable {
120
121
export function apiAssertTypes(paramsByType: { [type:string]: { [name:string]: any } }) {
122
for (const [types,params] of Object.entries(paramsByType))
123
- for (const type of types.split('_'))
124
- for (const [name,val] of Object.entries(params))
125
- if (type === 'array' ? !Array.isArray(val) : typeof val !== type)
126
- throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
123
+ for (const [name,val] of Object.entries(params))
124
+ if (! types.split('_').some(type => type === 'array' ? Array.isArray(val) : typeof val === type))
125
+ throw new ApiError(HTTP_BAD_REQUEST, 'bad ' + name)
126
}
\ No newline at end of file
src/persistence.ts
new
+11
@@ -0,0 +1,11 @@
1
+import { KvStorage } from '@rejetto/kvstorage'
2
+import { MINUTE, onProcessExit } from './misc'
3
+
4
+export const storedMap = new KvStorage({
5
+ defaultPutDelay: 5000,
6
+ maxPutDelay: MINUTE,
7
+ maxPutDelayCreate: 1000,
8
+ rewriteLater: true
9
+})
10
+storedMap.open('data.kv')
11
+onProcessExit(() => storedMap.flush())
src/throttler.ts
+18
-13
@@ -8,6 +8,7 @@ import { getOrSet, isLocalHost } from './misc'
8
import { Connection, getConnection, updateConnection } from './connections'
9
import _ from 'lodash'
10
import events from './events'
11
+import { storedMap } from './persistence'
12
13
const mainThrottleGroup = new ThrottleGroup(Infinity)
14
@@ -64,7 +65,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
65
conn[SymTimeout] = setTimeout(update, DELAY)
66
}, DELAY, { leading: true, maxWait:DELAY })
67
ts.on('sent', (n: number) => {
67
- totalSent += n
68
+ totalSent.set(x => (x || 0) + n)
69
update()
70
})
71
@@ -96,26 +97,30 @@ export function roundSpeed(n: number) {
97
return _.round(n, 1) || _.round(n, 3) // further precision if necessary
98
}
99
99
-export let totalSent = 0
100
-export let totalGot = 0
100
+export let totalSent = storedMap.singleSync<number>('totalSent', 0)
101
+export let totalGot = storedMap.singleSync<number>('totalGot', 0)
102
export let totalOutSpeed = 0
103
export let totalInSpeed = 0
104
104
-let lastSent = totalSent
105
-let lastGot = totalGot
105
+let lastSent: number | undefined
106
+let lastGot: number | undefined
107
let last = Date.now()
108
setInterval(() => {
109
const now = Date.now()
109
- const past = (now - last) / 1000 // seconds
110
+ const past = now - last
111
last = now
111
- const deltaSentKb = (totalSent - lastSent) / 1000
112
- lastSent = totalSent
113
- const deltaGotKb = (totalGot - lastGot) / 1000
114
- lastGot = totalGot
115
- totalOutSpeed = roundSpeed(deltaSentKb / past)
116
- totalInSpeed = roundSpeed(deltaGotKb / past)
112
+ {
113
+ const v = totalSent.get()
114
+ totalOutSpeed = roundSpeed((v - (lastSent ?? v)) / past)
115
+ lastSent = v
116
+ }
117
+ {
118
+ const v = totalGot.get()
119
+ totalInSpeed = roundSpeed((v - (lastGot ?? v)) / past)
120
+ lastGot = v
121
+ }
122
}, 1000)
123
124
events.on('connection', (c: Connection) =>
125
c.socket.on('data', data =>
121
- totalGot += data.length ))
126
+ totalGot.set(x => (x || 0) + data.length) ))