Skip to content

Commit 936baf3

Browse files
authored
Publish 1.0.1 Version
2 parents a113dbf + 5b358ec commit 936baf3

File tree

11 files changed

+206
-70
lines changed

11 files changed

+206
-70
lines changed

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
A tool for CN XCPC contests
44

5-
- 全平台远程代码打印
6-
- 支持封榜期间发放鼓励气球的小票机
5+
- 代码打印和小票机打印(全平台支持)
6+
- 支持封榜期间发放鼓励气球
7+
- 支持连接 DOMjudge 与 Hydro 系统,同时亦可独立运行
78
- 选手机赛时数据监控与屏幕监控
89

910
TODO Features:
1011

1112
- [ ] 更好的选手机座位绑定
12-
- [ ] 支持多平台参赛系统
13-
- [ ] Windows 小票机支持
1413
- [ ] 优化 UI 顺畅度
1514
- [ ] 使用 WebSocket 返回指令执行情况
1615
- [ ] 支持全考场监视
@@ -51,7 +50,7 @@ const serverSchema = Schema.intersect([
5150
Schema.const('hydro'),
5251
] as const).required(), // 赛事系统类型
5352
server: Schema.string().role('url').required(), // 赛事系统地址
54-
contestId: Schema.string(), // 赛事ID,如无则自动获取(Domjudge),hydro 请使用 domainId/contestId 作为ID
53+
contestId: Schema.string(), // 赛事ID,如无则自动获取(DOMjudge),hydro 请使用 domainId/contestId 作为ID
5554
token: Schema.string(), // 赛事系统 Token 如无可使用用户名密码登录
5655
username: Schema.string(), // 赛事系统用户名
5756
password: Schema.string(), // 赛事系统密码
@@ -72,7 +71,7 @@ const serverSchema = Schema.intersect([
7271
`print [file] [original] [language] [username] [teamname] [teamid] [location]` 为打印命令,其中 `file` 为代码文件路径,`original` 为原文件名,`language` 为语言,`username` 为用户名,`teamname` 为队伍名,`teamid` 为队伍ID,`location` 为选手位置。
7372

7473
#### Balloon
75-
服务支持 `Fetch Mode` 下的气球推送,支持 `Domjudge``Hydro` 系统,支持 `Domjudge``Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。
74+
服务支持 `Fetch Mode` 下的气球推送,支持 `DOMjudge``Hydro` 系统,支持 `DOMjudge``Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。
7675

7776
#### Monitor
7877
服务支持监控选手机情况和监控服务器桌面,如您需要选手机监控,可通过设置 Systemd 定时执行任务等多种方式定时执行 `monitor` 命令,如需监控服务器桌面,请在选手机上提前运行 `vlc-camera``vlc-desktop` 服务, CAICPC 镜像已经内置了这三两个服务,您只需在选手机上运行即可,如您为自己的镜像,可从 `https://github.com/hydro-dev/xcpc-tools/blob/main/scripts/monitor` 下载 `monitor` 服务。

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"packages/*"
77
],
88
"scripts": {
9-
"start:client": "node -r ./register.js packages/server/index.ts --client --debug",
10-
"start:server": "node -r ./register.js packages/server/index.ts --debug",
9+
"dev:client": "node -r ./register.js packages/server/index.ts --client --debug",
10+
"dev:server": "node -r ./register.js packages/server/index.ts --debug",
1111
"lint": "eslint packages --ext js,ts,tsx,jsx",
12-
"build": "yarn build:ui:prod && node -r ./register.js build.ts",
12+
"build": "yarn build:ui && node -r ./register.js build.ts",
1313
"build:server": "node -r ./register.js build.ts",
14-
"build:pkg": "yarn build:ui:prod && node -r ./register.js build.ts && pkg dist/xcpc-tools.js --targets linux,macos,win --out-path dist/pkg"
14+
"build:pkg": "yarn build:ui && node -r ./register.js build.ts && pkg dist/xcpc-tools.js --targets linux,macos,win --out-path dist/pkg"
1515
},
1616
"devDependencies": {
1717
"@expo-google-fonts/noto-color-emoji": "^0.2.3",

packages/server/client/balloon.ts

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
/* eslint-disable no-await-in-loop */
2-
import path from 'path';
32
import EscPosEncoder from '@freedom_sky/esc-pos-encoder';
43
import superagent from 'superagent';
54
import { config } from '../config';
65
import {
7-
convertToChinese,
8-
fs, Logger, sleep,
6+
checkReceiptStatus, convertToChinese, Logger, receiptPrint, sleep,
97
} from '../utils';
108

119
const encoder = new EscPosEncoder();
@@ -72,30 +70,6 @@ const logger = new Logger('balloon');
7270
let timer = null;
7371
let printer = null;
7472

75-
async function getReceiptStatus(receipt) {
76-
const lp = receipt.split('/').pop();
77-
const oldPrinter = printer;
78-
printer = {
79-
printer: receipt,
80-
info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(),
81-
};
82-
if (!oldPrinter || oldPrinter.info === printer.info) return;
83-
logger.info('Printer changed:', printer.printer, printer.info);
84-
const usbDevices = fs.readdirSync('/dev/usb');
85-
for (const f of usbDevices) {
86-
if (f.startsWith('lp')) {
87-
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
88-
if (lpid === oldPrinter.info) {
89-
logger.info('Printer found:', f, ':', lpid);
90-
oldPrinter.printer = `/dev/usb/${f}`;
91-
printer = oldPrinter;
92-
break;
93-
}
94-
}
95-
}
96-
if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.');
97-
}
98-
9973
async function printBalloon(doc, lang) {
10074
const bReceipt = receiptText(
10175
doc.balloonid,
@@ -107,10 +81,8 @@ async function printBalloon(doc, lang) {
10781
doc.total ? Object.keys(doc.total).map((k) => `- ${k}: ${doc.total[k].color}`).join('\n') : 'N/A',
10882
lang,
10983
);
110-
if (printer) {
111-
await getReceiptStatus(printer.printer);
112-
fs.writeFileSync(path.resolve(printer.printer), bReceipt);
113-
}
84+
printer = await checkReceiptStatus(printer);
85+
await receiptPrint(printer, bReceipt);
11486
}
11587

11688
async function fetchTask(c) {
@@ -137,7 +109,7 @@ async function fetchTask(c) {
137109
}
138110

139111
export async function apply() {
140-
await getReceiptStatus(config.balloon);
112+
printer = config.balloon;
141113
if (config.token && config.server && config.balloon) await fetchTask(config);
142114
else logger.error('Config not found, please check the config.yaml');
143115
}

packages/server/config.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'path';
22
import Schema from 'schemastery';
33
import { version } from './package.json';
44
import {
5+
checkReceiptPrinter,
56
fs, getPrinters, Logger, yaml,
67
} from './utils';
78

@@ -31,24 +32,15 @@ password:
3132
`;
3233
let printers = [];
3334
if (isClient) {
34-
printers = await getPrinters().then((r) => r.map((p) => p.printer)).catch(() => []);
35+
printers = await getPrinters(true).catch(() => []);
3536
logger.info(printers.length, 'printers found:', printers.join(', '));
36-
if (process.platform === 'linux') {
37-
const usbDevices = fs.readdirSync('/dev/usb');
38-
for (const f of usbDevices) {
39-
if (f.startsWith('lp')) {
40-
const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim();
41-
logger.info(`USB Printer ${f} found: ${lpid}`);
42-
logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`);
43-
}
44-
}
45-
} else logger.info('If you want to use balloon client, please run this on linux.');
37+
await checkReceiptPrinter(printers);
4638
}
4739
const clientConfigDefault = yaml.dump({
4840
server: '',
4941
balloon: '',
5042
balloonLang: 'zh',
51-
printers,
43+
printers: printers.map((p) => p.printer),
5244
token: '',
5345
});
5446
fs.writeFileSync(configPath, isClient ? clientConfigDefault : serverConfigDefault);
@@ -81,6 +73,7 @@ const serverSchema = Schema.intersect([
8173
token: Schema.string(),
8274
username: Schema.string(),
8375
password: Schema.string(),
76+
freezeEncourage: Schema.number().default(0),
8477
}).description('Fetcher Config'),
8578
Schema.object({
8679
type: Schema.const('server').required(),

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hydrooj/xcpc-tools",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "A tools for XCPC contests",
55
"main": "index.ts",
66
"repository": "https://github.com/Hydro-dev/xcpc-tools",

packages/server/service/fetcher.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Context, Service } from 'cordis';
33
import superagent from 'superagent';
44
import { config } from '../config';
5-
import { Logger } from '../utils';
5+
import { Logger, mongoId } from '../utils';
66

77
const logger = new Logger('fetcher');
88
const fetch = (url: string, type: 'get' | 'post' = 'get') => superagent[type](new URL(url, config.server).toString())
@@ -11,10 +11,10 @@ export interface IBasicFetcher {
1111
contest: Record<string, any>
1212
cron(): Promise<void>
1313
contestInfo(): Promise<boolean>
14-
getToken?(username: string, password: string): Promise<void>
15-
teamInfo?(): Promise<void>
16-
balloonInfo?(all: boolean): Promise<void>
17-
setBalloonDone?(bid: string): Promise<void>
14+
getToken(username: string, password: string): Promise<void>
15+
teamInfo(): Promise<void>
16+
balloonInfo(all: boolean): Promise<void>
17+
setBalloonDone(bid: string): Promise<void>
1818
}
1919
class BasicFetcher extends Service implements IBasicFetcher {
2020
contest: any;
@@ -60,7 +60,7 @@ class BasicFetcher extends Service implements IBasicFetcher {
6060
}
6161
}
6262

63-
class DomJudgeFetcher extends BasicFetcher {
63+
class DOMjudgeFetcher extends BasicFetcher {
6464
async contestInfo() {
6565
let contest;
6666
if (!config.contestId) {
@@ -141,10 +141,96 @@ class DomJudgeFetcher extends BasicFetcher {
141141
}
142142
}
143143

144+
class HydroFetcher extends BasicFetcher {
145+
async contestInfo() {
146+
const ids = config.contestId.split('/');
147+
const [domainId, contestId] = ids.length === 2 ? ids : ['system', config.contestId];
148+
const { body } = await fetch(`/d/${domainId}/contest/${contestId}`);
149+
if (!body || !body.tdoc) {
150+
logger.error('Contest not found');
151+
return false;
152+
}
153+
const contest = body.tdoc;
154+
contest.freeze_time = contest.lockAt;
155+
const old = this?.contest?._id;
156+
this.contest = {
157+
info: contest, id: contest._id, name: contest.title, domainId,
158+
};
159+
logger.info(`Connected to ${contest.name}(id=${contest.id})`);
160+
return old === this.contest.id;
161+
}
162+
163+
async getToken(username, password) {
164+
const res = await fetch('/login', 'post').send({ uname: username, password, rememberme: 'on' })
165+
.redirects(0).ok((i) => i.status === 302);
166+
if (!res) throw new Error('Failed to get token');
167+
config.token = `Bearer ${res.header['set-cookie'][0].split(';')[0].split('=')[1]}`;
168+
}
169+
170+
async teamInfo() {
171+
const { body } = await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/user`);
172+
if (!body || !body.length) return;
173+
const teams = body.tsdocs.filter((t) => body.udict[t.uid]).map((t) => (body.udict[t.uid]));
174+
for (const team of teams) {
175+
await this.ctx.db.teams.update({ id: team._id }, { $set: team }, { upsert: true });
176+
}
177+
logger.debug(`Found ${teams.length} teams`);
178+
}
179+
180+
async balloonInfo(all) {
181+
if (all) logger.info('Sync all balloons...');
182+
const { body } = await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/balloon?todo=${all ? 'false' : 'true'}`);
183+
if (!body || !body.length) return;
184+
const balloons = body;
185+
for (const balloon of balloons) {
186+
const teamTotal = await this.ctx.db.balloon.find({ teamid: balloon.teamid, time: { $lt: (balloon.time * 1000).toFixed(0) } });
187+
const encourage = teamTotal.length < (config.freezeEncourage ?? 0);
188+
const totalDict = {};
189+
for (const t of teamTotal) {
190+
totalDict[t.problem] = t.contestproblem;
191+
}
192+
const shouldPrint = this.contest.info.freeze_time ? (balloon.time * 1000) < this.contest.info.freeze_time || encourage : true;
193+
if (!shouldPrint && !balloon.done) await this.setBalloonDone(balloon.balloonid);
194+
const contestproblem = {
195+
id: String.fromCharCode(this.contest.pids.indexOf(balloon.pid) + 65),
196+
name: body.pdict[balloon.pid].title,
197+
rgb: this.contest.balloon[balloon.pid].color,
198+
color: this.contest.balloon[balloon.pid].name,
199+
};
200+
await this.ctx.db.balloon.update({ balloonid: balloon.balloonid }, {
201+
$set: {
202+
balloonid: balloon._id,
203+
time: mongoId(balloon._id).timestamp,
204+
problem: contestproblem.id,
205+
contestproblem,
206+
team: body.udict[balloon.uid].displayName,
207+
teamid: balloon.uid,
208+
location: body.udict[balloon.uid].studentId,
209+
affiliation: body.udict[balloon.uid].school,
210+
awards: balloon.first ? 'First of Problem' : (
211+
this.contest.info.freeze_time && (balloon.time * 1000) > this.contest.info.freeze_time
212+
&& encourage ? 'Encourage Balloon' : ''
213+
),
214+
done: balloon.sent,
215+
total: totalDict,
216+
printDone: balloon.done ? 1 : 0,
217+
shouldPrint,
218+
},
219+
}, { upsert: true });
220+
}
221+
logger.debug(`Found ${balloons.length} balloons`);
222+
}
223+
224+
async setBalloonDone(bid) {
225+
await fetch(`/d/${this.contest.domainId}/contest/${this.contest.id}/balloon`, 'post').send({ balloon: bid });
226+
logger.debug(`Balloon ${bid} set done`);
227+
}
228+
}
229+
144230
const fetcherList = {
145231
server: BasicFetcher,
146-
domjudge: DomJudgeFetcher,
147-
hydro: BasicFetcher, // TODO: HydroFetcher
232+
domjudge: DOMjudgeFetcher,
233+
hydro: HydroFetcher,
148234
};
149235

150236
export async function apply(ctx) {

packages/server/utils/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ export function sleep(timeout: number) {
8484
});
8585
}
8686

87+
// https://github.com/andrasq/node-mongoid-js/blob/master/mongoid.js
88+
export function mongoId(idstring: string) {
89+
if (typeof idstring !== 'string') idstring = String(idstring);
90+
return {
91+
timestamp: parseInt(idstring.slice(0, 0 + 8), 16),
92+
machineid: parseInt(idstring.slice(8, 8 + 6), 16),
93+
pid: parseInt(idstring.slice(14, 14 + 4), 16),
94+
sequence: parseInt(idstring.slice(18, 18 + 6), 16),
95+
};
96+
}
97+
8798
export * as fs from 'fs-extra';
8899
export * as yaml from 'js-yaml';
89100
export { Logger };
@@ -102,3 +113,4 @@ export function decodeBinary(file: string) {
102113
export * from './commandRunner';
103114
export * from './printers';
104115
export * from './color';
116+
export * from './receipt';

packages/server/utils/printers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export function initWinPrinter() {
3636
});
3737
}
3838

39-
const windowsPrinterStatus = {
39+
export const windowsPrinterStatus = {
4040
3: 'idle',
4141
4: 'printing',
4242
};
4343

44-
export async function getPrinters(): Promise<Printer[]> {
44+
export async function getPrinters(raw = false): Promise<object[]> {
4545
if (process.platform === 'win32') {
4646
const winprinters = await wingetPrinters();
47-
return winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
47+
return raw ? winprinters : winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({
4848
printer: p.DeviceID,
4949
description: p.Caption,
5050
status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown',

0 commit comments

Comments
 (0)