Skip to content

Commit f6cc2f8

Browse files
committed
support windows for print code
1 parent 35525e1 commit f6cc2f8

File tree

7 files changed

+138
-106
lines changed

7 files changed

+138
-106
lines changed

packages/server/config.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ if (!fs.existsSync(configPath)) {
2020
const serverConfigDefault = `\
2121
type: server # server | domjudge | hydro
2222
viewPass: ${String.random(8)} # use admin / viewPass to login
23+
secretRoute: ${String.random(12)}
24+
seatFile: /home/icpc/Desktop/seat.txt
25+
# if type is server, the following is not needed
2326
server:
2427
token:
2528
username:
2629
password:
27-
secretRoute: ${String.random(12)}
28-
seatFile: /home/icpc/Desktop/seat.txt
2930
`;
3031

3132
const clientConfigDefault = yaml.dump({
@@ -55,16 +56,15 @@ const serverSchema = Schema.intersect([
5556
}).description('Basic Config'),
5657
Schema.union([
5758
Schema.object({
58-
type: Schema.const('domjudge').required(),
59-
server: Schema.string().role('url').required(),
60-
username: Schema.string().required(),
61-
password: Schema.string().required(),
62-
}).description('DomJudge Fetcher Config'),
63-
Schema.object({
64-
type: Schema.const('hydro').required(),
59+
type: Schema.union([
60+
Schema.const('domjudge'),
61+
Schema.const('hydro'),
62+
] as const).required(),
6563
server: Schema.string().role('url').required(),
66-
token: Schema.string().required(),
67-
}).description('Hydro Fetcher Config'),
64+
token: Schema.string(),
65+
username: Schema.string(),
66+
password: Schema.string(),
67+
}).description('Fetcher Config'),
6868
Schema.object({
6969
type: Schema.const('server').required(),
7070
}).description('Server Mode Config'),

packages/server/handler/commands.ts

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,11 @@
11
/* eslint-disable no-empty-pattern */
22
/* eslint-disable no-await-in-loop */
3-
import child from 'child_process';
4-
import fs from 'fs';
5-
import { homedir } from 'os';
63
import { Context } from 'cordis';
74
import { param, Types } from '@hydrooj/framework';
85
import { config } from '../config';
9-
import { Logger } from '../utils';
6+
import { executeOnHost } from '../utils';
107
import { AuthHandler } from './misc';
118

12-
const logger = new Logger('handler/commands');
13-
14-
async function asyncCommand(command: string | string[], timeout = 10000) {
15-
let result = '';
16-
const proc = typeof command === 'string' ? child.exec(command) : child.spawn(command[0], command.slice(1));
17-
proc.stdout?.on('data', (d) => {
18-
result += d.toString();
19-
});
20-
proc.stderr?.on('data', (d: Buffer) => {
21-
logger.error(' STDERR ', d.toString());
22-
});
23-
if (!timeout) {
24-
proc.on('exit', () => { });
25-
setTimeout(() => proc.kill(), 1000);
26-
return '';
27-
}
28-
return await new Promise<string>((resolve, reject) => {
29-
const t = setTimeout(() => {
30-
proc.kill();
31-
reject(new Error('timeout'));
32-
}, timeout);
33-
proc.on('exit', (code) => {
34-
clearTimeout(t);
35-
if (code === 0) {
36-
resolve(result.replace(/\r/g, ''));
37-
} else {
38-
reject(code);
39-
}
40-
});
41-
});
42-
}
43-
44-
const keyfile = fs.existsSync(`${homedir()}.ssh/id_rsa`) ? '.ssh/id_rsa' : '.ssh/id_ed25519';
45-
46-
async function executeOnHost(host: string, command: string, timeout = 10000) {
47-
logger.info('executing', command, 'on', host);
48-
return await asyncCommand([
49-
'ssh', '-o', 'StrictHostKeyChecking no', '-o', `IdentityFile ~/${keyfile}`,
50-
`root@${host}`,
51-
'bash', '-c', `'echo $(echo ${Buffer.from(command).toString('base64')} | base64 -d | bash)'`,
52-
], timeout);
53-
}
54-
559
class CommandsHandler extends AuthHandler {
5610
async get() {
5711
this.response.body = '';

packages/server/service/fetcher.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface IBasicFetcher {
1111
contest: Record<string, any>
1212
cron(): Promise<void>
1313
contestInfo(): Promise<boolean>
14+
getToken?(username: string, password: string): Promise<void>
1415
teamInfo?(): Promise<void>
1516
balloonInfo?(all: boolean): Promise<void>
1617
setBalloonDone?(bid: string): Promise<void>
@@ -24,7 +25,12 @@ class BasicFetcher extends Service implements IBasicFetcher {
2425
}
2526

2627
async cron() {
28+
if (config.type === 'server') return;
2729
logger.info('Fetching contest info...');
30+
if (!config.token) {
31+
if (config.username && config.password) await this.getToken(config.username, config.password);
32+
else throw new Error('No token or username/password provided');
33+
}
2834
const first = await this.contestInfo();
2935
if (first) await this.teamInfo();
3036
await this.balloonInfo(first);
@@ -36,6 +42,10 @@ class BasicFetcher extends Service implements IBasicFetcher {
3642
return old === this.contest.id;
3743
}
3844

45+
async getToken(username, password) {
46+
config.token = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
47+
}
48+
3949
async teamInfo() {
4050
logger.debug('Found 0 teams');
4151
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* eslint-disable @typescript-eslint/no-loop-func */
2+
import child from 'child_process';
3+
import fs from 'fs';
4+
import { homedir } from 'os';
5+
import { Logger } from './index';
6+
7+
const logger = new Logger('runner');
8+
9+
export async function asyncCommand(command: string | string[], timeout = 10000) {
10+
let result = '';
11+
const proc = typeof command === 'string' ? child.exec(command) : child.spawn(command[0], command.slice(1));
12+
proc.stdout?.on('data', (d) => {
13+
result += d.toString();
14+
});
15+
proc.stderr?.on('data', (d: Buffer) => {
16+
logger.error(' STDERR ', d.toString());
17+
});
18+
if (!timeout) {
19+
proc.on('exit', () => { });
20+
setTimeout(() => proc.kill(), 1000);
21+
return '';
22+
}
23+
return await new Promise<string>((resolve, reject) => {
24+
const t = setTimeout(() => {
25+
proc.kill();
26+
reject(new Error('timeout'));
27+
}, timeout);
28+
proc.on('exit', (code) => {
29+
clearTimeout(t);
30+
if (code === 0) {
31+
resolve(result.replace(/\r/g, ''));
32+
} else {
33+
reject(code);
34+
}
35+
});
36+
});
37+
}
38+
39+
const keyfile = fs.existsSync(`${homedir()}.ssh/id_rsa`) ? '.ssh/id_rsa' : '.ssh/id_ed25519';
40+
41+
export async function executeOnHost(host: string, command: string, timeout = 10000) {
42+
logger.info('executing', command, 'on', host);
43+
return await asyncCommand([
44+
'ssh', '-o', 'StrictHostKeyChecking no', '-o', `IdentityFile ~/${keyfile}`,
45+
`root@${host}`,
46+
'bash', '-c', `'echo $(echo ${Buffer.from(command).toString('base64')} | base64 -d | bash)'`,
47+
], timeout);
48+
}

packages/server/utils.ts renamed to packages/server/utils/index.ts

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-loop-func */
22
/* eslint-disable no-await-in-loop */
3-
import { spawn } from 'child_process';
43
import { gunzipSync } from 'zlib';
54
import { decode } from 'base16384';
65
import Logger from 'reggol';
@@ -94,46 +93,11 @@ export function StaticHTML(context, randomHash) {
9493
return `<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@Hydro/XCPC-TOOLS</title></head><body><div id="root"></div><script>window.Context=JSON.parse('${JSON.stringify(context)}')</script><script src="/main.js?${randomHash}"></script></body></html>`;
9594
}
9695

97-
// wait for undefined to write
98-
export async function remoteRunner(user: string, target: string, targetPort: string, timeout = 10, RETRY = 3, command) {
99-
let log = '';
100-
const defaultCommand = `-o ConnectTimeout=${timeout} -o StrictHostKeyChecking=no -P ${targetPort}`;
101-
const cmds = {
102-
exec: [defaultCommand, `${user}@${target}`, command],
103-
upload: [defaultCommand, command.from, `${user}@${target}:${command.to}`],
104-
download: [defaultCommand, `${user}@${target}:${command.from.replace('{target}', target)}`, command.to.replace('{target}', target)],
105-
};
106-
let retry = 0;
107-
while (retry < RETRY) {
108-
const child = spawn(command.type === 'exec' ? 'ssh' : 'scp', cmds[command.type]);
109-
// 输出命令行执行的结果
110-
let success = false;
111-
child.stdout.on('data', (data) => {
112-
success = true;
113-
log += data;
114-
});
115-
child.stderr.on('data', (data) => {
116-
success = false;
117-
log += data;
118-
});
119-
// 执行命令行错误
120-
child.on('error', (err) => {
121-
log += err;
122-
return { success: false, log };
123-
});
124-
// 命令行执行结束
125-
child.on('close', (e) => {
126-
if (e === 0) return { success: true, log };
127-
if (success) return { success: true, log };
128-
log += `retry ${retry} times`;
129-
retry++;
130-
});
131-
}
132-
return { success: false, log };
133-
}
134-
13596
export function decodeBinary(file: string) {
13697
if (process.env.NODE_ENV === 'development') return Buffer.from(file, 'base64');
13798
const buf = decode(file);
13899
return gunzipSync(buf);
139100
}
101+
102+
export * from './commandRunner';
103+
export * from './printers';

packages/server/utils/printers.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import fs from 'fs';
2+
import winPrint from '@myteril/node-win-printer';
3+
import unixPrint from 'unix-print';
4+
import path from 'path';
5+
6+
let winPrinter: winPrint.PDFPrinter;
7+
8+
if (process.platform === 'win32') {
9+
const execPath = [
10+
'./SumatraPDF.exe',
11+
path.resolve(__dirname, 'SumatraPDF.exe'),
12+
path.resolve(process.cwd(), 'SumatraPDF.exe'),
13+
'C:\\Program Files\\SumatraPDF\\SumatraPDF.exe',
14+
'C:\\Program Files (x86)\\SumatraPDF\\SumatraPDF.exe',
15+
];
16+
const sumatraPdfPath = execPath.find((p) => fs.existsSync(p));
17+
if (!sumatraPdfPath) {
18+
throw new Error('SumatraPDF not found, please install it on https://www.sumatrapdfreader.org/download-free-pdf-viewer');
19+
}
20+
winPrinter = new winPrint.PDFPrinter({
21+
sumatraPdfPath,
22+
});
23+
}
24+
25+
export async function getPrinters() {
26+
if (process.platform === 'win32') {
27+
return winPrint.getPrinters();
28+
}
29+
return unixPrint.getPrinters();
30+
}
31+
32+
export async function print(printer, file) {
33+
if (process.platform === 'win32') {
34+
return winPrinter.print({
35+
file,
36+
printer,
37+
});
38+
}
39+
return unixPrint.print(printer, file);
40+
}

packages/ui/app/pages/Commands.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
2+
import { Button, Card, Divider, Group, Textarea, Title } from '@mantine/core';
23
import { notifications } from '@mantine/notifications';
34

45
export default function Commands() {
5-
const [command, setCommand] = React.useState('')
6+
const [command, setCommand] = React.useState('');
67

78
const operation = async (op: string) => {
89
try {
@@ -20,15 +21,30 @@ export default function Commands() {
2021
console.error(e);
2122
notifications.show({ title: 'Error', message: 'Failed to update balloon', color: 'red' });
2223
}
23-
}
24+
};
2425

2526
return (
26-
<div>
27-
<textarea rows={10} cols={100} style={{ fontFamily: 'monospace' }} value={command} onChange={(ev) => setCommand(ev.target.value)} />
28-
<button onClick={() => fetch('/commands', { method: 'POST', body: JSON.stringify({ command }) })}>Send</button>
29-
<button onClick={() => operation('reboot')}>Reboot All</button>
30-
<button onClick={() => operation('set_hostname')}>Update Hostname</button>
31-
<button onClick={() => operation('show_ids')}>Show IDS</button>
32-
</div >
27+
<Card shadow="sm" padding="lg" radius="md" withBorder>
28+
<Title order={3}>Send Commands to Monitoring Computer</Title>
29+
<Textarea
30+
label="Command"
31+
my="md"
32+
rows={10}
33+
cols={100}
34+
style={{ fontFamily: 'monospace' }}
35+
value={command}
36+
onChange={(ev) => setCommand(ev.target.value)}
37+
/>
38+
<Group justify="center" my="md">
39+
<Button onClick={() => fetch('/commands', { method: 'POST', body: JSON.stringify({ command }) })}>Send</Button>
40+
</Group>
41+
<Divider my="md" />
42+
<Title order={3}>Quick Commands</Title>
43+
<Group my="md" justify="space-start">
44+
<Button onClick={() => operation('reboot')}>Reboot All</Button>
45+
<Button onClick={() => operation('set_hostname')}>Update Hostname</Button>
46+
<Button onClick={() => operation('show_ids')}>Show IDS</Button>
47+
</Group>
48+
</Card>
3349
);
3450
}

0 commit comments

Comments
 (0)