diff --git a/README.md b/README.md index f487c09..b49fb65 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,14 @@ A tool for CN XCPC contests -- 全平台远程代码打印 -- 支持封榜期间发放鼓励气球的小票机 +- 代码打印和小票机打印(全平台支持) +- 支持封榜期间发放鼓励气球 +- 支持连接 DOMjudge 与 Hydro 系统,同时亦可独立运行 - 选手机赛时数据监控与屏幕监控 TODO Features: - [ ] 更好的选手机座位绑定 -- [ ] 支持多平台参赛系统 -- [ ] Windows 小票机支持 - [ ] 优化 UI 顺畅度 - [ ] 使用 WebSocket 返回指令执行情况 - [ ] 支持全考场监视 @@ -51,7 +50,7 @@ const serverSchema = Schema.intersect([ Schema.const('hydro'), ] as const).required(), // 赛事系统类型 server: Schema.string().role('url').required(), // 赛事系统地址 - contestId: Schema.string(), // 赛事ID,如无则自动获取(Domjudge),hydro 请使用 domainId/contestId 作为ID + contestId: Schema.string(), // 赛事ID,如无则自动获取(DOMjudge),hydro 请使用 domainId/contestId 作为ID token: Schema.string(), // 赛事系统 Token 如无可使用用户名密码登录 username: Schema.string(), // 赛事系统用户名 password: Schema.string(), // 赛事系统密码 @@ -72,7 +71,7 @@ const serverSchema = Schema.intersect([ `print [file] [original] [language] [username] [teamname] [teamid] [location]` 为打印命令,其中 `file` 为代码文件路径,`original` 为原文件名,`language` 为语言,`username` 为用户名,`teamname` 为队伍名,`teamid` 为队伍ID,`location` 为选手位置。 #### Balloon -服务支持 `Fetch Mode` 下的气球推送,支持 `Domjudge` 与 `Hydro` 系统,支持 `Domjudge` 与 `Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。 +服务支持 `Fetch Mode` 下的气球推送,支持 `DOMjudge` 与 `Hydro` 系统,支持 `DOMjudge` 与 `Hydro` 系统的 `Balloon` 推送,同时若赛事在封榜后仍然推送气球,则支持自定义鼓励气球数,高于设定值则不推送,为所有队伍打造优质赛场体验。 #### Monitor 服务支持监控选手机情况和监控服务器桌面,如您需要选手机监控,可通过设置 Systemd 定时执行任务等多种方式定时执行 `monitor` 命令,如需监控服务器桌面,请在选手机上提前运行 `vlc-camera` 和 `vlc-desktop` 服务, CAICPC 镜像已经内置了这三两个服务,您只需在选手机上运行即可,如您为自己的镜像,可从 `https://github.com/hydro-dev/xcpc-tools/blob/main/scripts/monitor` 下载 `monitor` 服务。 diff --git a/packages/server/client/balloon.ts b/packages/server/client/balloon.ts index 3eb6c3e..d6a65d5 100644 --- a/packages/server/client/balloon.ts +++ b/packages/server/client/balloon.ts @@ -1,12 +1,9 @@ /* eslint-disable no-await-in-loop */ -import { exec } from 'child_process'; -import path from 'path'; import EscPosEncoder from '@freedom_sky/esc-pos-encoder'; import superagent from 'superagent'; import { config } from '../config'; import { - convertToChinese, - fs, Logger, sleep, + checkReceiptStatus, convertToChinese, Logger, receiptPrint, sleep, } from '../utils'; const encoder = new EscPosEncoder(); @@ -73,31 +70,6 @@ const logger = new Logger('balloon'); let timer = null; let printer = null; -async function getReceiptStatus(receipt) { - if (process.platform === 'win32') printer = { printer: receipt }; - const lp = receipt.split('/').pop(); - const oldPrinter = printer; - printer = { - printer: receipt, - info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(), - }; - if (!oldPrinter || oldPrinter.info === printer.info) return; - logger.info('Printer changed:', printer.printer, printer.info); - const usbDevices = fs.readdirSync('/dev/usb'); - for (const f of usbDevices) { - if (f.startsWith('lp')) { - const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim(); - if (lpid === oldPrinter.info) { - logger.info('Printer found:', f, ':', lpid); - oldPrinter.printer = `/dev/usb/${f}`; - printer = oldPrinter; - break; - } - } - } - if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.'); -} - async function printBalloon(doc, lang) { const bReceipt = receiptText( doc.balloonid, @@ -109,17 +81,8 @@ async function printBalloon(doc, lang) { doc.total ? Object.keys(doc.total).map((k) => `- ${k}: ${doc.total[k].color}`).join('\n') : 'N/A', lang, ); - if (printer) { - await getReceiptStatus(printer.printer); - if (process.platform === 'win32') { - fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), bReceipt); - exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => { - if (err) logger.error(err); - if (stdout) logger.info(stdout); - if (stderr) logger.error(stderr); - }); - } else fs.writeFileSync(path.resolve(printer.printer), bReceipt); - } + printer = await checkReceiptStatus(printer); + await receiptPrint(printer, bReceipt); } async function fetchTask(c) { @@ -146,7 +109,7 @@ async function fetchTask(c) { } export async function apply() { - await getReceiptStatus(config.balloon); + printer = config.balloon; if (config.token && config.server && config.balloon) await fetchTask(config); else logger.error('Config not found, please check the config.yaml'); } diff --git a/packages/server/config.ts b/packages/server/config.ts index 87ec2a7..e18db2f 100644 --- a/packages/server/config.ts +++ b/packages/server/config.ts @@ -2,8 +2,8 @@ import path from 'path'; import Schema from 'schemastery'; import { version } from './package.json'; import { - fs, getPrinters, getWinReceiptPrinter, - Logger, yaml, + checkReceiptPrinter, + fs, getPrinters, Logger, yaml, } from './utils'; const logger = new Logger('init'); @@ -32,32 +32,15 @@ password: `; let printers = []; if (isClient) { - printers = await getPrinters().then((r) => r.map((p) => p.printer)).catch(() => []); + printers = await getPrinters(true).catch(() => []); logger.info(printers.length, 'printers found:', printers.join(', ')); - if (process.platform === 'linux') { - const usbDevices = fs.readdirSync('/dev/usb'); - for (const f of usbDevices) { - if (f.startsWith('lp')) { - const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim(); - logger.info(`USB Printer ${f} found: ${lpid}`); - logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`); - } - } - if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.'); - } else if (process.platform === 'win32') { - const printerList = await getWinReceiptPrinter(); - for (const printer of printerList) { - logger.info(`Receipt Printer ${printer.printer}(${printer.device})) found: ${printer.description}`); - logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`); - } - if (!printers.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.'); - } else logger.info('If you want to use balloon client, please run this on Linux/Windows.'); + await checkReceiptPrinter(printers); } const clientConfigDefault = yaml.dump({ server: '', balloon: '', balloonLang: 'zh', - printers, + printers: printers.map((p) => p.printer), token: '', }); fs.writeFileSync(configPath, isClient ? clientConfigDefault : serverConfigDefault); @@ -90,6 +73,7 @@ const serverSchema = Schema.intersect([ token: Schema.string(), username: Schema.string(), password: Schema.string(), + freezeEncourage: Schema.number().default(0), }).description('Fetcher Config'), Schema.object({ type: Schema.const('server').required(), diff --git a/packages/server/package.json b/packages/server/package.json index a14c5f7..1bb1768 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/xcpc-tools", - "version": "1.0.0", + "version": "1.0.1", "description": "A tools for XCPC contests", "main": "index.ts", "repository": "https://github.com/Hydro-dev/xcpc-tools", diff --git a/packages/server/utils/index.ts b/packages/server/utils/index.ts index 7fea24e..832fa05 100644 --- a/packages/server/utils/index.ts +++ b/packages/server/utils/index.ts @@ -113,3 +113,4 @@ export function decodeBinary(file: string) { export * from './commandRunner'; export * from './printers'; export * from './color'; +export * from './receipt'; diff --git a/packages/server/utils/printers.ts b/packages/server/utils/printers.ts index ebad73b..1ffbba5 100644 --- a/packages/server/utils/printers.ts +++ b/packages/server/utils/printers.ts @@ -36,15 +36,15 @@ export function initWinPrinter() { }); } -const windowsPrinterStatus = { +export const windowsPrinterStatus = { 3: 'idle', 4: 'printing', }; -export async function getPrinters(): Promise { +export async function getPrinters(raw = false): Promise { if (process.platform === 'win32') { const winprinters = await wingetPrinters(); - return winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({ + return raw ? winprinters : winprinters.filter((p: any) => p.DeviceID).map((p: any) => ({ printer: p.DeviceID, description: p.Caption, status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown', @@ -63,13 +63,3 @@ export async function print(file: string, printer: string, startPage?: number, e } return unixPrint(file, printer, startPage && endPage ? ['-P', `${startPage}-${endPage}`] : []); } - -export async function getWinReceiptPrinter() { - const winprinters = await wingetPrinters(); - return winprinters.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({ - printer: `\\\\${p.SystemName}\\${p.ShareName}`, - device: p.DeviceID, - description: p.Caption, - status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown', - })); -} diff --git a/packages/server/utils/receipt.ts b/packages/server/utils/receipt.ts new file mode 100644 index 0000000..df0aeac --- /dev/null +++ b/packages/server/utils/receipt.ts @@ -0,0 +1,74 @@ +import { exec } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { Logger, windowsPrinterStatus } from '.'; + +const logger = new Logger('receipt'); + +export async function checkReceiptPrinter(printers: object[]) { + if (process.platform === 'linux') { + const usbDevices = fs.readdirSync('/dev/usb'); + for (const f of usbDevices) { + if (f.startsWith('lp')) { + const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim(); + logger.info(`USB Printer ${f} found: ${lpid}`); + logger.info(`If you want to use this printer for balloon print, please set balloon: /dev/usb/${f} in config.yaml.`); + } + } + if (!usbDevices.length) logger.info('If you want to use balloon client, please connect your receipt printer first.'); + } else if (process.platform === 'win32') { + const shared = printers.filter((p: any) => p.DeviceID).filter((p: any) => p.ShareName).map((p: any) => ({ + printer: `\\\\${p.SystemName}\\${p.ShareName}`, + device: p.DeviceID, + description: p.Caption, + status: windowsPrinterStatus[p.PrinterStatus] ? windowsPrinterStatus[p.PrinterStatus] : 'unknown', + })); + for (const printer of shared) { + logger.info(`Receipt Shared Printer ${printer.printer}(${printer.device})) found: ${printer.description}`); + logger.info(`If you want to use this printer for balloon print, please set balloon: ${printer.printer} in config.yaml.`); + } + if (!shared.length) logger.info('If you want to use balloon client, please share your receipt printer on settings first.'); + } else if (process.platform === 'darwin') { + logger.info('If you want to use balloon client, please set balloon: "{printer name}" in config.yaml.'); + } else logger.info('If you want to use balloon client, please run this on Linux/Windows/MacOS'); +} + +export async function checkReceiptStatus(printer) { + if (process.platform !== 'linux') { + printer = { printer: printer.printer }; + return; + } + const lp = printer.printer.split('/').pop(); + const oldPrinter = printer; + printer = { + printer: printer.printer, + info: fs.readFileSync(`/sys/class/usbmisc/${lp}/device/ieee1284_id`, 'utf8').trim(), + }; + if (!oldPrinter || oldPrinter.info === printer.info) return; + logger.info('Printer changed:', printer.printer, printer.info); + const usbDevices = fs.readdirSync('/dev/usb'); + for (const f of usbDevices) { + if (f.startsWith('lp')) { + const lpid = fs.readFileSync(`/sys/class/usbmisc/${f}/device/ieee1284_id`, 'utf8').trim(); + if (lpid === oldPrinter.info) { + logger.info('Printer found:', f, ':', lpid); + oldPrinter.printer = `/dev/usb/${f}`; + printer = oldPrinter; + break; + } + } + } + if (oldPrinter.info !== printer.info) throw Error('Printer not found, please check the printer connection.'); +} + +export async function receiptPrint(text, printer) { + fs.writeFileSync(path.resolve(process.cwd(), 'data', 'balloon.txt'), text); + if (process.platform === 'win32') { + exec(`COPY /B "${path.resolve(process.cwd(), 'data', 'balloon.txt')}" "${printer.printer}"`, (err, stdout, stderr) => { + if (err) logger.error(err); + if (stdout) logger.info(stdout); + if (stderr) logger.error(stderr); + }); + } else if (process.platform === 'darwin') exec(`lpr -P ${printer.printer} -o raw ${path.resolve(process.cwd(), 'data', 'balloon.txt')}`); + else fs.writeFileSync(path.resolve(printer.printer), text); +}