fix: admin/logs: server-side errors retrieving log were not displayed
Massimo Melina committed
Aug 9, 2022 at 12:01 UTC
3bb5c0d707c1a8cb6112d1e8a219cef4c4f37e95
4 files changed
+54
-32
admin/src/api.ts
+3
-3
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, useCallback, useEffect, useMemo, useRef } from 'react'
4
-import { Dict, Falsy, getCookie, IconBtn, spinner, useStateMounted } from './misc'
4
+import { Dict, err2msg, Falsy, getCookie, IconBtn, spinner, useStateMounted, wantArray } from './misc'
5
import { Alert } from '@mui/material'
6
import _ from 'lodash'
7
import { state } from './state'
@@ -173,13 +173,13 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
173
case 'msg':
174
if (src?.readyState === src?.CLOSED)
175
return stop()
176
- if (data === 'end') {
176
+ if (data === 'ready') {
177
flush()
178
setInitializing(false)
179
return
180
}
181
if (data.error)
182
- return setError(data.error)
182
+ return setError(err2msg(data.error))
183
if (data.add) {
184
const rec = map(data.add)
185
if (addId)
server/src/adminApis.ts
+26
-19
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { ApiError, ApiHandlers } from './apiMiddleware'
3
+import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
4
import { defineConfig, getWholeConfig, setConfig } from './config'
5
import { getStatus, getUrls, httpsPortCfg, portCfg } from './listen'
6
import {
@@ -23,7 +23,6 @@ import _ from 'lodash'
23
import events from './events'
24
import { getFromAccount } from './perm'
25
import Koa from 'koa'
26
-import { Readable } from 'stream'
26
import { getProxyDetected } from './middlewares'
27
import { writeFile } from 'fs/promises'
28
import { createReadStream } from 'fs'
@@ -91,23 +90,31 @@ export const adminApis: ApiHandlers = {
90
},
91
92
async get_log({ file }, ctx) {
94
- const logger = loggers.find(l => l.name === file)
95
- if (!logger)
96
- return new ApiError(404)
97
- const ret = new Readable({ objectMode: true, read(){} })
98
- const input = createReadStream(logger.path)
99
- readline.createInterface({ input }).on('line', line => {
100
- if (ctx.aborted)
101
- return input.close()
102
- ret.push({ add: parse(line) })
103
- }).on('close', () => // file is automatically closed, so we continue by events
104
- ctx.res.once('close', onOff(events, { // unsubscribe when connection is interrupted
105
- [logger.name](entry) {
106
- ret.push({ add: entry })
107
- }
108
- })))
109
-
110
- return ret
93
+ return new SendListReadable(list => {
94
+ const logger = loggers.find(l => l.name === file)
95
+ if (!logger)
96
+ return list.error(404)
97
+ const input = createReadStream(logger.path)
98
+ input.on('error', async (e: any) => {
99
+ if (e.code !== 'ENOENT') // ignore ENOENT, consider it an empty log
100
+ return list.error(e.code || e.message)
101
+ })
102
+ input.on('ready', () => {
103
+ list.ready()
104
+ readline.createInterface({ input }).on('line', line => {
105
+ if (ctx.aborted)
106
+ return input.close()
107
+ const obj = parse(line)
108
+ if (obj)
109
+ list.add(obj)
110
+ }).on('close', () => // file is automatically closed, so we continue by events
111
+ ctx.res.once('close', onOff(events, { // unsubscribe when connection is interrupted
112
+ [logger.name](entry) {
113
+ list.add(entry)
114
+ }
115
+ })) )
116
+ })
117
+ })
118
119
function parse(line: string) {
120
const m = /^(.+?) - (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (.+)$/.exec(line)
server/src/api.plugins.ts
+1
-1
@@ -110,7 +110,7 @@ const apis: ApiHandlers = {
110
catch (err: any) {
111
list.error(err.code || err.message)
112
}
113
- list.end()
113
+ list.ready()
114
})
115
return list
116
},
server/src/apiMiddleware.ts
+24
-9
@@ -32,8 +32,13 @@ export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
32
: await apis[ctx.path](params || {}, ctx)
33
if (isAsyncGenerator(res))
34
res = asyncGeneratorToReadable(res)
35
- if (res instanceof Readable) // Readable, we'll go SSE-mode
36
- return res.pipe(createSSE(ctx))
35
+ if (res instanceof Readable) { // Readable, we'll go SSE-mode
36
+ res.pipe(createSSE(ctx))
37
+ const stillRes = res // satisfy ts
38
+ ctx.req.on('close', () => // by closing the generated stream, creator of the stream will know the request is over without having to access anything else
39
+ stillRes.destroy())
40
+ return
41
+ }
42
if (res instanceof ApiError) {
43
ctx.body = res.message
44
return ctx.status = res.status
@@ -68,15 +73,22 @@ async function getJsonFromReq(req: IncomingMessage): Promise<any> {
73
}
74
75
// offer an api for a generic dynamic list. Suitable to be the result of an api.
76
+type SendListFunc<T> = (list:SendListReadable<T>) => void
77
export class SendListReadable<T> extends Readable {
78
protected lastError: string | number | undefined
73
- constructor(addAtStart?: T[]) {
79
+ constructor(addOrDoAtStart?: T[] | SendListFunc<T>) {
80
super({ objectMode: true, read(){} })
75
- if (addAtStart) {
76
- for (const x of addAtStart)
77
- this.add(x)
78
- this.end()
81
+ this.on('end', () =>
82
+ this.destroy())
83
+ if (!addOrDoAtStart)
84
+ return
85
+ if (typeof addOrDoAtStart === 'function') {
86
+ setTimeout(() => addOrDoAtStart(this))
87
+ return
88
}
89
+ for (const x of addOrDoAtStart)
90
+ this.add(x)
91
+ this.ready()
92
}
93
add(rec: T) {
94
this.push({ add: rec })
@@ -90,8 +102,8 @@ export class SendListReadable<T> extends Readable {
102
close() {
103
this.push(null)
104
}
93
- end() { // useful to indicate the end of an initial phase, but we leave open for updates
94
- this.push('end')
105
+ ready() { // useful to indicate the end of an initial phase, but we leave open for updates
106
+ this.push('ready')
107
}
108
error(msg: NonNullable<typeof this.lastError>) {
109
this.push({ error: msg })
@@ -108,4 +120,7 @@ export class SendListReadable<T> extends Readable {
120
ctx.res.once('close', off)
121
return this
122
}
123
+ isClosed() {
124
+ return this.destroyed
125
+ }
126
}