Skip to content

Commit 37a0411

Browse files
committed
monitor page performance update & send command through heartbeat script
1 parent 9db3cd1 commit 37a0411

15 files changed

Lines changed: 661 additions & 214 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const serverSchema = Schema.intersect([
4141
port: Schema.number().default(5283), // 服务端口
4242
viewPass: Schema.string().default(String.random(8)), // UI登录密码,可通过 admin / {viewPass} 登录
4343
secretRoute: Schema.string().default(String.random(12)), // 打印路径,用于远程调用
44-
seatFile: Schema.string().default('/home/icpc/Desktop/seat.txt'), // 选手座位绑定文件
4544
}).description('Basic Config'),
4645
Schema.union([
4746
Schema.object({

packages/server/config.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path';
22
import Schema from 'schemastery';
3+
import { Config } from './handler/monitor';
34
import { version as packageVersion } from './package.json';
45
import {
56
checkReceiptPrinter,
@@ -47,7 +48,6 @@ if (!fs.existsSync(configPath)) {
4748
type: server # server | domjudge | hydro
4849
viewPass: ${randomstring(8)} # use admin / viewPass to login
4950
secretRoute: ${randomstring(12)}
50-
seatFile: /home/icpc/Desktop/seats.txt
5151
customKeyfile:
5252
# if type is server, the following is not needed
5353
server:
@@ -91,11 +91,8 @@ const serverSchema = Schema.intersect([
9191
xhost: Schema.string().default('x-forwarded-host'),
9292
viewPass: Schema.string().default(randomstring(8)),
9393
secretRoute: Schema.string().default(randomstring(12)),
94-
seatFile: Schema.string().default('/home/icpc/Desktop/seat.txt'),
9594
customKeyfile: Schema.string().default(''),
96-
monitor: Schema.object({
97-
timeSync: Schema.boolean().default(false),
98-
}).default({ timeSync: false }),
95+
monitor: Config,
9996
}).description('Basic Config'),
10097
Schema.union([
10198
Schema.object({

packages/server/handler/commands.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,49 @@ import { AuthHandler } from './misc';
66

77
class CommandsHandler extends AuthHandler {
88
async get() {
9-
this.response.body = '';
9+
const commands = await this.ctx.db.command.find({}).sort({ time: -1 }).limit(100);
10+
const monitors = await this.ctx.db.monitor.find({});
11+
const monitorMap = new Map(monitors.map((m) => [m.mac, m]));
12+
const commandsWithInfo = commands.map((cmd) => ({
13+
_id: cmd._id,
14+
command: cmd.command,
15+
target: cmd.target || [],
16+
executionResult: cmd.executionResult || {},
17+
targetInfo: (cmd.target || []).map((mac) => ({
18+
mac,
19+
hostname: monitorMap.get(mac)?.hostname || mac,
20+
name: monitorMap.get(mac)?.name || '',
21+
})),
22+
status: {
23+
total: cmd.target?.length || 0,
24+
completed: Object.keys(cmd.executionResult || {}).length,
25+
pending: (cmd.target?.length || 0) - Object.keys(cmd.executionResult || {}).length,
26+
},
27+
}));
28+
this.response.body = { commands: commandsWithInfo };
1029
}
1130

12-
async postCommand({ command }) {
31+
async postCommand({ command, target, mode = 'heartbeat' }) {
1332
if (!command || typeof command !== 'string') throw new BadRequestError('Command', null, 'Command is required');
14-
this.response.body = this.executeForAll(command);
33+
if (mode === 'heartbeat') {
34+
target ||= (await this.ctx.db.monitor.find({})).map((m) => m.mac);
35+
target = target.map((i) => i.replace(/:/g, ''));
36+
const res = await this.ctx.db.command.insert({
37+
command,
38+
time: Date.now(),
39+
target,
40+
pending: target,
41+
executionResult: {},
42+
});
43+
this.response.body = { id: res._id };
44+
} else {
45+
this.response.body = this.executeForAll(command);
46+
}
47+
}
48+
49+
async postRemove({ command }) {
50+
await this.ctx.db.command.deleteOne({ _id: command }, {});
51+
this.response.body = { success: true };
1552
}
1653

1754
async executeForAll(command: string, t = 10000) {
@@ -33,11 +70,7 @@ else
3370
export DISPLAY=:0
3471
fi
3572
export XAUTHORITY=/run/user/1000/gdm/Xauthority
36-
zenity --info --text "<span font='256'>$(cat ${config.seatFile})</span>"`);
37-
}
38-
39-
async postSetHostname() {
40-
this.response.body = await this.executeForAll(`hostnamectl hostname $(cat -- ${config.seatFile})`);
73+
zenity --info --text "<span font='256'>$(cat /etc/hostname)</span>"`);
4174
}
4275

4376
async postAutologin({ }, autologin = false) {

packages/server/handler/monitor.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import path from 'path';
12
import { Context, Schema } from 'cordis';
3+
import fs from 'fs-extra';
24
import { BadRequestError, Handler } from '@hydrooj/framework';
5+
import { Logger } from '../utils';
36
import { AuthHandler } from './misc';
47

8+
const logger = new Logger('monitor');
9+
const actions = fs.createWriteStream(path.join(process.cwd(), 'data/actions.log'), { flags: 'a' });
10+
511
class MonitorAdminHandler extends AuthHandler {
612
async get(params) {
713
const { nogroup } = params;
@@ -90,14 +96,23 @@ class MonitorAdminHandler extends AuthHandler {
9096
}
9197
}
9298

93-
async function saveMonitorInfo(ctx: Context, monitor: any) {
99+
const escape = (str: string) => str.trim().replace(/"/g, '\\"').replace(/\r/g, '').replace(/\n/g, '\\n');
100+
101+
async function saveMonitorInfo(ctx: Context, monitor: any, config) {
94102
const {
95103
mac, version, uptime, seats, ip,
96104
os, kernel, cpu, cpuused, mem, memused, load,
105+
window_cmdline, window_exe, window_name,
97106
} = monitor;
107+
logger.debug('save monitor info %o', monitor);
108+
actions.write(`${Date.now()},${seats},"${escape(window_cmdline)}","${escape(window_exe)}","${escape(window_name)}"\n`);
98109
const monitors = await ctx.db.monitor.find({ mac });
99110
const warn = monitors.length > 1 || (monitors.length && monitors[0].ip !== ip);
100111
if (warn) ctx.logger('monitor').warn(`Duplicate monitor ${mac} from (${ip}, ${monitors.length ? monitors[0].ip : 'null'})`);
112+
const autoGroupPayload = (config.autoGroup && /^[A-Z][0-9]+$/.test(seats)) ? {
113+
group: seats[0],
114+
name: seats,
115+
} : {};
101116
await ctx.db.monitor.updateOne({ mac }, {
102117
$set: {
103118
mac,
@@ -114,13 +129,15 @@ async function saveMonitorInfo(ctx: Context, monitor: any) {
114129
...mem && { mem },
115130
...mem && { memUsed: memused },
116131
...load && { load },
132+
...autoGroupPayload,
117133
},
118134
}, { upsert: true });
119135
}
120136

121137
export const Config = Schema.object({
122138
timeSync: Schema.boolean().default(false),
123-
});
139+
autoGroup: Schema.boolean().default(false),
140+
}).default({ timeSync: false, autoGroup: false });
124141

125142
export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
126143
class MonitorReportHandler extends Handler {
@@ -132,8 +149,49 @@ export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
132149
if (!params.mac) throw new BadRequestError();
133150
params.ip = this.request.ip.replace('::ffff:', '');
134151
if (params.mac === '00:00:00:00:00:00') throw new BadRequestError('Invalid MAC address');
135-
await saveMonitorInfo(this.ctx, params);
136-
this.response.body = `#!/bin/bash\n${config.timeSync ? `date "${new Date().toISOString()}"` : 'echo Success'}`;
152+
await saveMonitorInfo(this.ctx, params, config);
153+
if (this.request.files?.file) {
154+
const resultContent = fs.readFileSync(this.request.files.file.filepath, 'utf-8');
155+
const commandResults: Map<string, string> = new Map();
156+
let currentCommandId: string | null = null;
157+
let currentOutput: string[] = [];
158+
const lines = resultContent.split('\n');
159+
for (const line of lines) {
160+
const startMatch = line.match(/^---COMMAND_START:(.+?)---$/);
161+
const endMatch = line.match(/^---COMMAND_END:(.+?)---$/);
162+
if (startMatch) {
163+
currentCommandId = startMatch[1];
164+
currentOutput = [];
165+
} else if (endMatch && currentCommandId === endMatch[1]) {
166+
commandResults.set(currentCommandId, currentOutput.join('\n') || '(No output)');
167+
currentCommandId = null;
168+
currentOutput = [];
169+
} else if (currentCommandId) {
170+
currentOutput.push(line);
171+
}
172+
}
173+
if (currentCommandId) commandResults.set(currentCommandId, currentOutput.join('\n') || '(No output)');
174+
await Promise.all(Array.from(commandResults.entries()).map(async ([commandId, output]) => {
175+
const cmd = await ctx.db.command.findOne({ _id: commandId });
176+
if (cmd) {
177+
const executionResult = cmd.executionResult || {};
178+
executionResult[params.mac] = output;
179+
const newPending = cmd.pending.filter((t: string) => t !== params.mac);
180+
await ctx.db.command.updateOne({ _id: commandId }, { $set: { executionResult, pending: newPending } });
181+
}
182+
}));
183+
}
184+
const scriptParts: string[] = [
185+
'#!/bin/bash',
186+
config.timeSync ? `date --set="${new Date().toISOString()}"` : 'echo Time sync disabled',
187+
];
188+
for (const cmd of await ctx.db.command.find({ pending: params.mac })) {
189+
scriptParts.push(`echo ---COMMAND_START:${cmd._id}---`);
190+
scriptParts.push(cmd.command);
191+
scriptParts.push(`echo ---COMMAND_END:${cmd._id}---`);
192+
}
193+
this.response.body = `${scriptParts.join('\n')}\n`;
194+
this.response.type = 'text/x-shellscript';
137195
}
138196
}
139197
ctx.Route('monitor_report', '/report', MonitorReportHandler);

packages/server/interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ export interface MonitorDoc {
4848
desktop?: string;
4949
}
5050

51+
export interface CommandTask {
52+
_id: string;
53+
time: number;
54+
command: string;
55+
target: string[];
56+
pending: string[];
57+
executionResult: Record<string, string>;
58+
}
59+
5160
export interface ClientDoc {
5261
_id: string;
5362
id: string;

packages/server/service/db.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path';
22
import { Context, Service } from 'cordis';
33
import Datastore from 'nedb-promises';
44
import {
5-
BalloonDoc, ClientDoc, MonitorDoc, PrintCodeDoc, TeamDoc,
5+
BalloonDoc, ClientDoc, CommandTask, MonitorDoc, PrintCodeDoc, TeamDoc,
66
} from '../interface';
77
import { fs } from '../utils';
88

@@ -12,6 +12,7 @@ export interface Collections {
1212
client: ClientDoc;
1313
balloon: BalloonDoc;
1414
teams: TeamDoc;
15+
command: CommandTask;
1516
}
1617

1718
declare module 'cordis' {
@@ -43,6 +44,7 @@ export default class DBService extends Service {
4344
await this.initDatabase('monitor', ['_id', 'mac', 'name', 'group']);
4445
await this.initDatabase('client', ['id', 'name', 'type', 'group']);
4546
await this.initDatabase('balloon', ['id', 'time', 'problem', 'teamid', 'awards', 'done', 'printDone']);
47+
await this.initDatabase('command', ['_id', 'command', 'target', 'pending', 'time', 'executionResult']);
4648
await this.initDatabase('teams', []);
4749
}
4850
}

packages/server/service/fetcher.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint-disable no-await-in-loop */
2+
import path from 'node:path';
23
import { Context, Service } from 'cordis';
34
import superagent from 'superagent';
45
import { config } from '../config';
5-
import { fs, Logger, mongoId, sleep } from '../utils';
6-
import path from 'node:path';
6+
import {
7+
fs, Logger, mongoId, sleep,
8+
} from '../utils';
79

810
const logger = new Logger('fetcher');
911
const fetch = (url: string, type: 'get' | 'post' = 'get') => {
@@ -266,9 +268,10 @@ class HydroFetcher extends BasicFetcher {
266268

267269
async printInfo(all) {
268270
const doFetch = async () => {
269-
const { body } = await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/print`, 'post').send({ operation: 'allocate_print_task' });
271+
const { body } = await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/print`, 'post')
272+
.send({ operation: 'allocate_print_task' });
270273
return body;
271-
}
274+
};
272275
let { task, udoc } = await doFetch();
273276
let cnt = 0;
274277
while (task) {
@@ -293,7 +296,8 @@ class HydroFetcher extends BasicFetcher {
293296
}
294297

295298
async setPrintDone(pid) {
296-
await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/print`, 'post').send({ operation: 'update_print_task', taskId: pid, status: 'printed' });
299+
await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/print`, 'post')
300+
.send({ operation: 'update_print_task', taskId: pid, status: 'printed' });
297301
this.logger.debug(`Print ${pid} set done`);
298302
}
299303
}

packages/server/service/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Context } from 'cordis';
22
import proxy from 'koa-proxies';
3-
import { ForbiddenError, HydroError, NotFoundError, UserFacingError, WebService } from '@hydrooj/framework';
3+
import {
4+
ForbiddenError, HydroError, NotFoundError, UserFacingError, WebService,
5+
} from '@hydrooj/framework';
6+
import { errorMessage } from '@hydrooj/utils';
47
import { config } from '../config';
58
import { randomstring } from '../utils';
6-
import { errorMessage } from '@hydrooj/utils';
79
export * from '@hydrooj/framework/decorators';
810

911
export async function apply(pluginContext: Context) {
@@ -62,7 +64,6 @@ export async function apply(pluginContext: Context) {
6264
error.msg ||= () => error.message;
6365
if (error instanceof UserFacingError && !process.env.DEV) error.stack = '';
6466
if (!(error instanceof NotFoundError) && !('nolog' in error)) {
65-
// eslint-disable-next-line max-len
6667
console.error(`${this.request.method}: ${this.request.path}`, error.msg(), error.params);
6768
if (error.stack) console.error(error.stack);
6869
}

0 commit comments

Comments
 (0)