-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcpsetup
433 lines (360 loc) · 14.5 KB
/
cpsetup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
#!/bin/sh
#
# This is an example script designed to show a possible use of the program
# `caffeine` (https://github.com/thud/caffeine).
#
# It provides the following (configurable) functionality.
# - Download testcases for a given contest and place into files.
# - Generate solution files from a template.
# - Poll user submissions, notifying the user on submission events.
#
# This script assumes that you have a folder structure similar to the following
# format (though it can be easily configured differently):
#
# |-- template.cpp
# |-- contest_name
# | |-- a.cpp
# | |-- ain
# | |-- b.cpp
# | |-- bin
# | |-- c1.cpp
# | |-- c1in
# | |-- c2.cpp
# | |-- c2in
# | |-- c2in2
# | |-- d.cpp
# | |-- e.cpp
# | |-- ein
# | |-- ein2
# | |-- ein3
# | `-- ein4
# .
# .
# .
#
# It is made capable of any folder structure or programming language by having
# adjustable filenames. However, this example is setup to follow the structure
# given above which requires you to have a template file in the parent dir of
# the contest directory.
#
USAGE="USAGE:
cpsetup <CONTESTNAME> [CONTESTID]";
HELP="cpsetup 0.1.0
thud <thud.dev>
An example script for setting up code for a Codeforces contest.
It automatically downloads testcases (placing them into labelled files);
creates a configurable folder structure for a contest; generates solutions
(from a template) based on defaults (and or fetched problem names); and polls
for changes in submissions, using notify-send to provide native notifications.
Note that this script relies on \`caffeine\` (https://github.com/thud/caffeine)
being installed and in your \$PATH for it to work.
$USAGE
FLAGS:
-h, --help Prints help information";
# The following constants allow you to configure the folder structure of this
# scripts output.
# Program to use for choosing the contest (if $2 empty).
SELECTOR_PROGRAM="fzf";
SOLUTION_FILE_EXTENSION=".cpp";
# Formatting of filenames for downloaded testcases.
TESTCASE_FN="<problem>in<num>";
# Formatting of filenames for solutions.
SOLUTION_FN="<problem>$SOLUTION_FILE_EXTENSION";
# Relative location of template file (assumed to be in the parent directory of
# the current contest folder). (see above)
SOLUTION_TEMPLATE_LOCATION="template$SOLUTION_FILE_EXTENSION";
# Solution files to generate straight away (before contest questions are
# available) (leave empty for none).
# eg. DEFAULT_SOLUTIONS="a\nb\nc\nd\ne\nf";
DEFAULT_SOLUTIONS="a";
# list of users to watch (leave empty for default user's friends).
USERS_TO_WATCH="";
# Relative location for storing cached data (in order to check when a change in
# standings/submissions has occurred.
CACHE_DIR="/tmp/codeforces/<contestid>";
# Time between polling Codeforces for submissions changes.
POLL_DELAY="60";
# Time between polling each individual Codeforces user for submissions changes.
# Try increasing if the Codeforces servers are returning rate-limit errors.
INTRA_POLL_DELAY="0";
eprintln() {
echo -e "[*] $*" 1>&2;
}
eprint() {
echo -en "$*" 1>&2;
}
eprintln_failed() {
echo -e "[E] $*" 1>&2;
}
# format the name of a testcase file (based on $TESTCASE_FN) and write given
# testcase data to file of that name.
# USAGE: write_testcases "edu108" "c2" "3" "template_src_string"
write_testcase() {
mkdir -p "$1";
tc_index="$3";
[ "$3" = "1" ] && tc_index="";
tc_filename="$(echo "$TESTCASE_FN" |
sed "s/<problem>/$2/g; s/<num>/$tc_index/g")";
tc_filepath="$1/$tc_filename";
[ -e "$tc_filepath" ] && return 1;
[ -n "$4" ] && echo -e "$4" > "$tc_filepath";
return 0;
}
# generate solution files from template with names given by a newline delimited
# string.
# USAGE: generate_solution_files "edu108" "a\nb\nc\nd1\nd2"
generate_solution_files() {
mkdir -p "$1";
echo -e "$2" | while read -r pi
do
cp -n "$SOLUTION_TEMPLATE_LOCATION" "$1/$(echo "$SOLUTION_FN" |
sed "s/<problem>/$pi/g")"; # copy template to solution location.
done;
}
# parse the response from `caffeine contest testcases` and generate the
# necessary files from the data.
# USAGE: generate_from_problems "edu108"
generate_from_problems() {
eprintln "Writing solutions from template...";
eprintln "running \`caffeine contest testcases $contest_id\`";
caffeine contest testcases "$contest_id" > \
"$cache_location/__testcases__";
if [ "$?" != "0" ]; then
eprintln_failed "\`caffeine contest testcases\` failed. (ABORTING)";
exit 2;
fi
current_testcase="";
testcase_index="0";
new_problem="0";
problem_names="test";
last_problem_name="";
last_testcase_index="1";
eprint "[*] Writing testcases: ";
while IFS= read -r line
do
[ "$line" = "--- NEW PROBLEM ---" ] && new_problem="1" && continue;
[ "$new_problem" = "1" ] &&
last_problem_name="$problem_name" &&
problem_name="$(echo "$line" | tr '[:upper:]' '[:lower:]')" &&
problem_names="$problem_names\n$problem_name" &&
new_problem="0" &&
testcase_index="0" &&
continue;
if [ "$line" = "+++ NEW TESTCASE +++" ]; then
testcase_index="$((testcase_index + 1))";
[ "$testcase_index" = "2" ] && last_problem_name="$problem_name";
[ -n "$last_problem_name" ] &&
eprint "${last_problem_name}[$last_testcase_index], ";
write_testcase "$1" "$last_problem_name" \
"$last_testcase_index" "$current_testcase";
last_testcase_index="$testcase_index";
current_testcase="";
continue;
fi
[ -n "$current_testcase" ] &&
current_testcase="$current_testcase\n$line" ||
current_testcase="$line";
done < "$cache_location/__testcases__";
# (handle last testcase)
[ -n "$problem_name" ] && eprint "${problem_name}[$testcase_index]";
write_testcase "$1" "$problem_name" "$testcase_index" "$current_testcase";
echo "" 1>&2;
problem_names="$(echo -e "$problem_names" | awk '(NR>1)')";
generate_solution_files "$1" "$problem_names";
}
# get contest id by using the custom $SELECTOR_PROGRAM of choice (eg. fzf)
# (only used if contest_id not explicitly given as CLI argument).
# USAGE: get_contest_id
get_contest_id() {
eprintln "No contest id provided. Using $SELECTOR_PROGRAM to find id...";
full_contest_name="$(echo "$contests" |
awk '/name:/ {$1=""; print $0}' | # Grab name fields,
sed 's/\("\|^\s*\|\s*$\)//g' | # trim whitespace,
"$SELECTOR_PROGRAM")"; # pipe names into selector.
eprintln "$full_contest_name";
name_ln="$(echo "$contests" | # Find line number of contest name.
grep -n "$full_contest_name" |
awk '{print $0} NR==1{exit}')";
echo "$contests" | # Get contest id from line number.
awk -v l="$name_ln" 'NR == l - 1 {print $3}';
}
# get time to contest start by parsing the list of contests and finding the
# correct row/column for the relative start time of the contest.
# USAGE: get_contest_time_to_start
get_contest_time_to_start() {
id_ln="$(echo "$contests" | # Find line number of contest name.
grep -n "\- id: $contest_id" |
awk -F ':' '{print $1}')";
st="$(echo "$contests" | # Get contest start time.
awk -v l="$id_ln" 'NR == l + 6 {print $2}')";
[ "$st" != "~" ] && st="$((st * -1))";
echo "$st";
}
# get contest duration by parsing the list of contests and finding the correct
# row/column for the duration of the contest.
# USAGE: get_contest_duration
get_contest_duration() {
id_ln="$(echo "$contests" | # Find line number of contest name.
grep -n "\- id: $contest_id" |
awk -F ':' '{print $1}')"; # There was a change HERE
st="$(echo "$contests" | # Get contest start time.
awk -v l="$id_ln" 'NR == l + 4 {print $2}')";
echo "$st";
}
# get default user and their friends.
# USAGE: get_user_watch_list
get_user_watch_list() {
caffeine user info | awk '/- handle:/ { print $3 }' || return 1;
caffeine user friends | awk 'NR>1 { print $2 }';
}
# notify the user of new submissions.
# USAGE: notify_new_submission "user1" "C2" "WRONG_ANSWER"
notify_new_submission() {
notify-send "Codeforces Submission ($1)" "Problem: $2\n$3";
}
# notify the user of verdict changes on 'old' submissions.
# USAGE: notify_verdict_change "user1" "C2" "WRONG_ANSWER"
notify_verdict_change() {
notify-send "Codeforces Submission ($1)" "Problem: $2\n$3 \
(verdict change)";
}
# watch user submissions to detect when a submission has been made, notifying
# the user in each case.
# USAGE: watch_changes "1494"
watch_changes() {
eprintln "Watching for submissions changes";
# setup users to watch.
[ -z "$USERS_TO_WATCH" ] &&
eprintln_failed "No users to watch." &&
return 0;
utw_path="$(echo "$CACHE_DIR" | sed "s/<contestid>/users_to_watch/g")";
echo "$USERS_TO_WATCH" > "$utw_path";
# - main loop - #
while true
do
# check if end time is in the past (if so, quit).
[ -n "$contest_start_time" ] && [ "$contest_duration" != "~" ] &&
t="$(date '+%s')" &&
[ "$((t - contest_start_time))" -gt "$contest_duration" ] &&
eprintln "Contest has ended." &&
return 0;
# loop through user submissions
comma="0";
eprint "[*] Polling users: ";
while read -r user
do
[ "$comma" = "1" ] && eprint ", " || comma="1";
eprint "$user ";
# grab latest submission for the current user.
latest_submission="$(caffeine user status -n1 "$user")";
# check latest_submission is a real submission.
[ "$(echo "$latest_submission" | wc -l)" -lt 10 ] &&
eprint "[E]" &&
continue;
# check latest_submission was for the current contest.
submission_contest_id="$(echo "$latest_submission" |
awk '/contestId:/ { print $2; exit }')";
[ "$submission_contest_id" != "$1" ] &&
eprint "[ ]" &&
continue;
# get submission ids of latest submission and one before
latest_submission_id="$(echo "$latest_submission" |
awk '/- id:/ { print $3; exit }')";
prev_submission_id="";
[ -s "$cache_location/$user" ] &&
prev_submission_id="$(awk 'NR==1' "$cache_location/$user")";
# get submission verdicts of latest submission and one before
latest_submission_vd="$(echo "$latest_submission" |
awk '/verdict:/ { print $2; exit }')";
prev_submission_vd="$latest_submission_vd";
[ -s "$cache_location/$user" ] &&
prev_submission_vd="$(awk 'NR==2' "$cache_location/$user")";
if [ "$latest_submission_id" != "$prev_submission_id" ]; then
# if submission id changed, notify.
latest_submission_problem="$(echo "$latest_submission" |
awk '/index:/ { print $2; exit }')";
eprint "[+]";
notify_new_submission "$user" "$latest_submission_problem" \
"$latest_submission_vd";
elif [ "$latest_submission_vd" != "$prev_submission_vd" ]; then
# if submission verdict changed, notify.
latest_submission_problem="$(echo "$latest_submission" |
awk '/index:/ { print $2; exit }')";
eprint "[*]";
notify_verdict_change "$user" "$latest_submission_problem" \
"$latest_submission_vd";
else
eprint "[_]";
fi
# write new submission id and verdict to cache file.
echo -e "$latest_submission_id\n$latest_submission_vd" > \
"$cache_location/$user";
# sleep between users in case of API rate-limit errors.
sleep "$INTRA_POLL_DELAY";
done < "$utw_path";
echo "" 1>&2;
# sleep until next time.
sleep "$POLL_DELAY";
done;
}
[ "$(id -u)" = "0" ] &&
eprintln_failed "Please do not run as root." &&
exit 2;
if [ -z "$1" ] || [ -n "$3" ]; then
echo "$USAGE" 1>&2;
exit 1;
fi
if [ "$1" = "-h" ] || [ "$1" = "--help" ] ||
[ "$2" = "-h" ] || [ "$2" = "--help" ]; then
echo "$HELP" 1>&2;
exit 1;
fi
command -v 'caffeine' 2>&1 >/dev/null ||
(eprintln_failed "\`caffeine\` is not installed (or in \$PATH). \
See https://github.com/thud/caffeine" &&
exit 2);
# --- main program --- #
# get list of contests.
contests="$(caffeine contest list)";
# get id for contest.
contest_id="${2:-"$(get_contest_id)"}";
full_contest_name="";
[ -z "$contest_id" ] && eprintln_failed "Unable to find contest id." && exit 2;
eprintln "Found contest id: $contest_id";
# setup cache directory.
cache_location="$(echo $CACHE_DIR | sed "s/<contestid>/$contest_id/g")"
mkdir -p "$cache_location" ||
(eprintln_failed "Failed to create cache dir." && exit 2);
# get timing for the contest.
contest_time_to_start="$(get_contest_time_to_start)";
contest_start_time="";
t="$(date '+%s')";
[ "$contest_time_to_start" = "~" ] &&
eprintln_failed "No start time found for this contest." ||
contest_start_time="$((t + contest_time_to_start))";
[ "$contest_time_to_start" != "~" ] &&
([ "$contest_time_to_start" -gt "0" ] &&
eprintln "Contest starts in $contest_time_to_start seconds." ||
eprintln "Contest has already started.");
contest_duration="$(get_contest_duration)";
# generate default solutions if contest hasn't started yet, then sleep until
# start if possible.
contest_time_to_start_mins="$((contest_time_to_start / 60))";
[ "$contest_time_to_start" != "~" ] && [ "$contest_time_to_start" -gt "0" ] &&
eprintln "Generating default solutions (contest hasn't yet started)." &&
generate_solution_files "$1" "$DEFAULT_SOLUTIONS" &&
eprintln "Sleeping $contest_time_to_start_mins mins to start." &&
sleep "$contest_time_to_start" ||
eprintln "Not sleeping since time to start is invalid or in the past.";
# from here on, we are now inside the contest time (or after it has finished).
# generate solution files if haven't done so already.
generate_from_problems "$1" &&
eprintln "Generated problems from Codeforces succesfully." ||
([ -n "$DEFAULT_SOLUTIONS" ] &&
generate_solution_files "$1" "$DEFAULT_SOLUTIONS" &&
eprintln "Generated default solutions successfully.");
# poll submissions / standings changes
[ "$contest_time_to_start" != "~" ] &&
USERS_TO_WATCH="${USERS_TO_WATCH:-"$(get_user_watch_list)"}" &&
watch_changes "$contest_id";
eprintln "Done.";