Skip to content

Commit 8a62855

Browse files
committed
New "Interactive" task type.
1 parent 65022c6 commit 8a62855

50 files changed

Lines changed: 2053 additions & 69 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/nextest.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@ platform = 'cfg(target_os = "macos")'
66
retries = 3
77

88
[[profile.default.overrides]]
9-
filter = 'test(classic) or test(communication)'
9+
filter = 'test(classic) or test(communication) or test(interactive)'
1010
slow-timeout = { period = "20s", terminate-after = 20 }
11+
12+
[[profile.default.overrides]]
13+
filter = 'test(interactive_many)'
14+
threads-required = "num-test-threads"

.github/workflows/release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Install Deps
3030
run: |
3131
apt update
32-
apt install -yy libseccomp-dev libssl-dev build-essential fpc
32+
apt install -yy libseccomp-dev libssl-dev build-essential fpc lld
3333
- name: Install Rust
3434
uses: dtolnay/rust-toolchain@stable
3535
id: install-rust
@@ -44,6 +44,8 @@ jobs:
4444
open(os.getenv("GITHUB_ENV"), "a").write(f"ARTIFACT_SUFFIX={artifact_suffix}\n")
4545
4646
- name: Release build
47+
env:
48+
RUSTFLAGS: "-C linker=clang -C link-arg=-fuse-ld=lld"
4749
run: cargo build --release
4850

4951
- name: Generate autocompletion files

.github/workflows/rust.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
if: startsWith(matrix.os, 'ubuntu')
1717
run: |
1818
sudo apt update
19-
sudo apt install -yy libseccomp-dev libssl-dev build-essential fpc
19+
sudo apt install -yy libseccomp-dev libssl-dev build-essential fpc lld
2020
2121
- name: Disable testing Java
2222
if: startsWith(matrix.os, 'ubuntu')
@@ -33,9 +33,14 @@ jobs:
3333

3434
- uses: taiki-e/install-action@nextest
3535

36+
- name: Set RUSTFLAGS for Linux
37+
if: runner.os == 'Linux'
38+
run: echo "RUSTFLAGS=-C linker=clang -C link-arg=-fuse-ld=lld" >> $GITHUB_ENV
39+
3640
- name: cargo nextest run (tests)
3741
env:
3842
RUST_BACKTRACE: 1
43+
RUST_LOG: info
3944
run: cargo nextest run --workspace --no-fail-fast
4045

4146
clippy:

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ itertools = "0.14"
115115
lazy_static = "1.5"
116116
log = "0.4"
117117
mime_guess = "2.0"
118-
nix = "0.29"
118+
nix = { version = "0.29", features = ["poll"] }
119119
num_cpus = "1.17"
120120
paste = "1.0.15"
121121
pest = "2.8"
@@ -219,6 +219,8 @@ serde_yaml = { workspace = true }
219219
zip = { version = "7.4.0", default-features = false }
220220
typst-assets = { version = "0.14.2", features = ["fonts"] }
221221

222+
nix = { workspace = true }
223+
222224
axum = { workspace = true }
223225
bytes = { workspace = true }
224226
tokio = { workspace = true }

docs/ioi-task-format.typ

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ The following are the most commonly set keys in this file:
4141
seconds.
4242
- `memory_limit`: the maximum amount of memory that a solution can use, in
4343
mebibytes.
44+
- `controller_time_limit`: the maximum amount of time that the controller can run
45+
for, in seconds (defaults to `time_limit + 1.0`).
46+
- `controller_wall_time_limit`: the maximum amount of wall time that the
47+
controller can run for, in seconds (defaults to `controller_time_limit * 3 + 1.0`).
48+
- `controller_memory_limit`: the maximum amount of memory that the controller can
49+
use, in mebibytes (defaults to `memory_limit`).
50+
- `controller_process_limit`: the maximum number of processes the controller can
51+
spawn (defaults to 200).
52+
- `interactive_concurrent`: whether the solution processes are assumed to be
53+
concurrent (wall time max-ed, memory summed) or sequential (wall time sum-ed,
54+
memory max-ed). Defaults to `true`.
4455
- `score_precision`: the number of decimal digits to round scores for this task
4556
to (defaults to 0, i.e. integers).
4657
- `user_io`: set this value to `fifo_io` to have solutions in communication
@@ -490,11 +501,14 @@ we suggest including at least the following:
490501
= `check` folder
491502

492503
The `check` folder can contain either a `checker.<ext>` file, or a
493-
`manager.<ext>` file, or be empty.
504+
`manager.<ext>` file, or a `controller.<ext>` file, or be empty.
494505

495506
If it contains `manager.<ext>`, the task is interpreted as a communication
496507
task, for which details are given in #ref(<communication>).
497508

509+
If it contains `controller.<ext>`, the task is interpreted as an interactive
510+
task, for which details are given in #ref(<interactive>).
511+
498512
The `checker.<ext>` file will be compiled by `task-maker-rust` (if necessary),
499513
and gets executed both by `task-maker-rust` and by CMS with three command line
500514
arguments, in order:
@@ -608,6 +622,112 @@ static FILE *to_manager, *from_manager;
608622
int main(int argc, char **argv) {
609623
from_manager = fopen(argv[2], "r");
610624
to_manager = fopen(argv[1], "w");
611-
// Call into the contestant's solution here.
625+
// call into the contestant's solution here.
626+
}
627+
```
628+
629+
= Interactive tasks
630+
<interactive>
631+
632+
An interactive task is identified by the presence of a `check/controller.<ext>`
633+
file (or `cor/controller.<ext>`). This type of task is similar to a communication task, but the controller
634+
has full control over the execution of one or more solution processes.
635+
636+
The controller communicates with `task-maker-rust` via standard output and standard input,
637+
and reports the score and messages via standard error.
638+
639+
== Controller protocol
640+
641+
When the controller starts, it should print `START_SOLUTION` followed by a newline
642+
to standard output for each solution process it wants to start.
643+
644+
After each `START_SOLUTION`, it should read two integers from standard input,
645+
representing the file descriptors (fds) for writing to and reading from the
646+
solution, respectively.
647+
648+
The controller then communicates with the solution(s) using these fds (e.g.,
649+
using `fdopen`).
650+
651+
To report the score and messages, the controller must print to standard error
652+
using the following prefixes:
653+
- `SCORE: <score>`: the score of the testcase, as a float between `0.0` and `1.0`.
654+
- `USER_MESSAGE: <message>`: a message for the contestant.
655+
- `ADMIN_MESSAGE: <message>`: an optional message for the administrators.
656+
657+
Like checkers and managers, the special messages `translate:success`,
658+
`translate:wrong` and `translate:partial` are supported.
659+
660+
== Example controller (`controller.cpp`)
661+
662+
```cpp
663+
#include "controller_lib.h"
664+
665+
int handler(FILE *to_sol, FILE *from_sol) {
666+
// Interaction logic here...
667+
grade(1.0, "translate:success", nullptr);
668+
return 0;
669+
}
670+
671+
int main() {
672+
return start_one_solution(handler);
673+
}
674+
```
675+
676+
== `controller_lib.h`
677+
678+
```cpp
679+
#include <assert.h>
680+
#include <stdio.h>
681+
#include <stdlib.h>
682+
683+
__attribute__((noreturn)) void grade(double score, const char *msg,
684+
const char *admin_msg) {
685+
fprintf(stderr, "SCORE: %f\n", score);
686+
fprintf(stderr, "USER_MESSAGE: %s\n", msg);
687+
if (admin_msg) {
688+
fprintf(stderr, "ADMIN_MESSAGE: %s\n", admin_msg);
689+
}
690+
exit(0);
691+
}
692+
693+
int start_one_solution(int (*handler)(FILE *to_solution, FILE *from_solution)) {
694+
printf("START_SOLUTION\n");
695+
fflush(stdout);
696+
int fdin, fdout;
697+
if (scanf("%d %d", &fdin, &fdout) != 2) return 1;
698+
FILE *to_solution = fdopen(fdin, "w");
699+
FILE *from_solution = fdopen(fdout, "r");
700+
assert(to_solution);
701+
assert(from_solution);
702+
int ret = handler(to_solution, from_solution);
703+
fclose(to_solution);
704+
fclose(from_solution);
705+
return ret;
706+
}
707+
708+
int start_many_solutions(int (*handler)(FILE **to_solution,
709+
FILE **from_solution, int num),
710+
int num) {
711+
FILE **to_solution = (FILE **)malloc(sizeof(FILE *) * num);
712+
FILE **from_solution = (FILE **)malloc(sizeof(FILE *) * num);
713+
for (int i = 0; i < num; i++) {
714+
printf("START_SOLUTION\n");
715+
fflush(stdout);
716+
int fdin, fdout;
717+
if (scanf("%d %d", &fdin, &fdout) != 2) return 1;
718+
to_solution[i] = fdopen(fdin, "w");
719+
from_solution[i] = fdopen(fdout, "r");
720+
assert(to_solution[i]);
721+
assert(from_solution[i]);
722+
}
723+
int ret = handler(to_solution, from_solution, num);
724+
for (int i = 0; i < num; i++) {
725+
fclose(to_solution[i]);
726+
fclose(from_solution[i]);
727+
}
728+
free(to_solution);
729+
free(from_solution);
730+
return ret;
612731
}
613732
```
733+

src/sandbox.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ impl Default for ToolsSandboxRunner {
5757

5858
impl SandboxRunner for ToolsSandboxRunner {
5959
fn run(&self, config: SandboxConfiguration, pid: Arc<AtomicU32>) -> RawSandboxResult {
60-
match tools_sandbox_internal(&self.tools_path, config, pid) {
61-
Ok(res) => res,
62-
Err(e) => RawSandboxResult::Error(e.to_string()),
63-
}
60+
tools_sandbox_internal(&self.tools_path, config, pid).into()
6461
}
6562
}
6663

src/tools/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::env;
2+
use std::path::PathBuf;
23

34
use clap::Parser;
45
use task_maker_rust::error::NiceError;
@@ -16,6 +17,7 @@ use task_maker_rust::tools::opt::{Opt, Tool};
1617
use task_maker_rust::tools::reset::main_reset;
1718
use task_maker_rust::tools::sandbox::main_sandbox;
1819
use task_maker_rust::tools::server::main_server;
20+
use task_maker_rust::tools::task_controller::main_task_controller;
1921
use task_maker_rust::tools::task_info::main_task_info;
2022
use task_maker_rust::tools::terry_statement::main_terry_statement;
2123
use task_maker_rust::tools::worker::main_worker;
@@ -25,6 +27,11 @@ fn main() {
2527
if env::args().nth(1).as_deref() == Some("internal-sandbox") {
2628
return task_maker_rust::sandbox::main_sandbox();
2729
}
30+
if env::args().nth(1).as_deref() == Some("internal-controller") {
31+
let process_limit = env::args().nth(2).unwrap().parse().unwrap();
32+
let result_dir = PathBuf::from(env::args().nth(3).unwrap());
33+
return task_maker_exec::controller_keeper(process_limit, &result_dir);
34+
}
2835

2936
rustls::crypto::ring::default_provider()
3037
.install_default()
@@ -50,6 +57,7 @@ fn main() {
5057
Tool::ExportSolutionChecks(opt) => main_export_solution_checks(opt),
5158
Tool::ExportBooklet(opt) => main_export_booklet(opt),
5259
Tool::EvalServer(opt) => main_eval_server(opt),
60+
Tool::TaskController(opt) => main_task_controller(opt),
5361
}
5462
.nice_unwrap()
5563
}

src/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod opt;
1212
pub mod reset;
1313
pub mod sandbox;
1414
pub mod server;
15+
pub mod task_controller;
1516
pub mod task_info;
1617
pub mod terry_statement;
1718
pub mod worker;

src/tools/opt.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::tools::gen_autocompletion::GenAutocompletionOpt;
1313
use crate::tools::reset::ResetOpt;
1414
use crate::tools::sandbox::SandboxOpt;
1515
use crate::tools::server::ServerOpt;
16+
use crate::tools::task_controller::TaskControllerOpt;
1617
use crate::tools::task_info::TaskInfoOpt;
1718
use crate::tools::terry_statement::TerryStatementOpt;
1819
use crate::tools::worker::WorkerOpt;
@@ -65,4 +66,6 @@ pub enum Tool {
6566
ExportBooklet(ExportBookletOpt),
6667
/// Start a web server for evaluating arbitrary code.
6768
EvalServer(EvalServerOpt),
69+
/// Run a solution with an interactor.
70+
TaskController(TaskControllerOpt),
6871
}

0 commit comments

Comments
 (0)