First version

This commit is contained in:
andy committed 2026-10-15 20:44:05 -03:00
commit 243e92a9dd
84 files changed
+4012

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
build/
test_report/
tests/work/
# Local organizer notes must not be committed or included in handoff archives.
/Notes.local.md
/Notes.local.*
+304
View File
@@ -0,0 +1,304 @@
# ACPC 2026 contest image
This repository builds bootable USB sticks for ACPC 2026 at `atlantic-canada26.kattis.com`. It starts from the ICPC image `icpc2027-2026-09-29`, based on Ubuntu 26.04.1. Teams use an account without administrator rights, with compilers configured for the contest and local copies of the language documentation. The network and device restrictions run on each contest machine, so campus staff do not have to install a separate network firewall.
## Building the image
The build machine needs `qemu-base`, `qemu-img`, `edk2-ovmf`, `ansible`, `sshpass`, `e2fsprogs`, `fuse3` and `curl`. These are the Arch Linux package names. Hardware virtualization through KVM and about 30 GB of free disk space are also required. Run the build from a regular account, with a private administrator password in place of `PASSWORD`:
```
./build.sh --admin-pass 'PASSWORD'
```
The build downloads the ICPC image from image.icpc.global and compares its SHA-256 checksum with the value recorded in `build.sh`. This detects a download that differs from the expected image. Changes are written to `build/acpc2026.qcow2`, which stores the differences from the original image. Ansible configures that copy inside a virtual machine, then shuts it down.
| Option | Default | Meaning |
| --- | --- | --- |
| `--admin-pass PASS` | required | Password for the `icpc` administrator account and for editing the boot menu |
| `--team-pass PASS` | `contest` | Password for the `team` account |
| `--base-image FILE` | download | Use an existing, decompressed ICPC image, in raw `.img` format |
| `--fresh` | | Discard the previous build and start from the ICPC image |
| `--export` | | Also produce `build/acpc2026.img` for tools that require a raw disk image |
Without `--fresh`, the playbooks are applied to the existing build. Use this to incorporate changes such as an updated domain list. To inspect the result, run `./run_vm.sh`. Changes made in that test session are discarded when the virtual machine closes.
## Writing the sticks
Run the following command on the build machine, replacing the device names with the intended USB disks. It lists the disks and requires the word `ERASE` before overwriting them. Every file on those disks will be lost.
```
sudo ./write_sticks.sh build/acpc2026.qcow2 /dev/sdb /dev/sdc /dev/sdd
```
Only USB disks are accepted, and the selected disks are written in parallel. Omit the device names to list the USB disks without writing them. Keep the original ICPC image at its existing path because the qcow2 file depends on it.
## Accounts and administrator access
The full build separates the team account from the administrator account. Removing administrator rights from `team` prevents contestants from changing system files, installing system software or turning off the contest restrictions. Keep the administrator password with site staff.
| Account | Password | Access |
| --- | --- | --- |
| `team` | `contest`, or the value of `--team-pass` | Automatic desktop login, no sudo |
| `icpc` | The value of `--admin-pass` | Administrator access through sudo |
| `root` | Password login locked | Staff obtain root privileges through sudo from `icpc` |
During the build, Ansible receives passwords through a temporary variables file that only the build user can read. The file is deleted when the build script exits, and the tasks that set passwords suppress their values in Ansible output. These measures limit exposure of administrator credentials in build files and logs.
The build removes `team` from the sudo group and replaces the administrator password inherited from the ICPC image. It also removes the temporary SSH configuration used during the build. In the finished image, SSH password authentication is disabled, SSH login is denied to the `teams` and `judges` groups, and the firewall rejects new incoming SSH connections. These measures prevent remote access through the temporary build login or a contestant account.
### VS Code extension setup
The ICPC image includes a helper named `/usr/local/bin/vscode-extension-install`. It makes the preinstalled VS Code extensions available in a user's home directory and imports the certificate for the image's local extension server. This requires privileged filesystem mounts and ownership changes. The original image allowed every account to run that helper through sudo without a password, independently of membership in the sudo group.
The helper operates inside the calling user's home directory. A symbolic link is a filesystem reference to another path, and a user can create one in their home. A privileged helper that follows such a reference can modify files outside the intended directory. Removing `team` from the sudo group would therefore leave a separate route to privileged filesystem operations unless that special sudo permission was removed too.
`accounts.yml` deletes the special sudo rule and disables the helper's desktop autostart entry. The replacement, `team-vscode-extensions.service`, runs before the desktop starts and always prepares the account `team`. Its wrapper removes symbolic links at the certificate and extension paths it checks, refuses a home directory that is itself a symbolic link, and supplies the fixed user and group IDs of `team` to the original helper. Staff retain the preinstalled extensions without giving contestants permission to invoke the helper as root. The service also runs again after a staff member explicitly resets the team account.
### Boot menu and protected files
GRUB is the program that starts Linux before the login screen. The build requires the GRUB user `admin` and the administrator password to edit boot entries or use the GRUB command line. This prevents contestants from changing the Linux startup options to bypass the normal login and account restrictions. Normal Ubuntu startup requires no password, and recovery entries are removed.
The file `/etc/grub.d/01_acpc_password` contains a PBKDF2 hash derived from the administrator password. A hash lets GRUB check a password without storing the password itself. Someone with a copy could still test password guesses against it, so the file is readable and executable only by root. The same administrator password protects the `icpc` account, which is another reason to keep this file private and choose a strong password.
The installed firewall, maintenance and reset helpers are owned by root, with no read or execute permission for the team account. Staff run them through sudo. These permissions prevent contestants from invoking the privileged maintenance commands or replacing their installed contents.
## Languages
The contest languages are C, C++, Java and Python 3. Their compiler and interpreter versions match those displayed on the [ACPC Kattis language pages](https://atlantic-canada26.kattis.com/contests/atlantic-canada26/languages), checked on October 15, 2026. The expected versions and compiler options are recorded in `languages.yml` and `files/languages/`. The build checks the installed versions against those recorded values and stops if they differ.
| Language | Version | Commands |
| --- | --- | --- |
| C | gcc-15 15.2.0 | `compilec`, `runc` |
| C++ | g++-15 15.2.0 | `compilecpp`, `runcpp` |
| Java | javac and java 25.0.4.1 | `compilejava`, `runjava` |
| Python 3 | PyPy 7.3.23, implementing Python 3.11.15 | `compilepython3`, `runpython3` |
The ICPC image includes Java 25.0.4.1. Java is held at that version, and automatic package updates are disabled to keep compiler versions consistent across machines. When an existing build has a different Java version, the build installs the matching packages from the ICPC package repository and verifies each download against its recorded checksum.
The `compilec`, `compilecpp` and `compilejava` helpers add the compiler flags shown on the contest's Kattis pages. Teams can therefore compile with the judge's language standard, optimization, linking and encoding options without typing those flags themselves. For example, `compilecpp main.cpp` includes the GNU C++23, optimization and static linking flags used by Kattis. `compilegcc` and `compileg++` are aliases for the C and C++ helpers. The Python helper checks syntax with the configured PyPy interpreter.
The run helpers use the published runtime options. Java uses a maximum heap of 2048 MB by default, which can be changed for a local run with a command such as `MEMLIM=1024 runjava Main`. Local runs do not reproduce the judge's time and memory enforcement.
## IDEs and documentation
The image includes CLion, Code::Blocks, Eclipse, IntelliJ IDEA, PyCharm, VS Code and Geany. `desktop.yml` configures all 19 supported combinations of these IDEs and the contest languages. Defaults are installed in the team home and in `/etc/skel`, the directory used to create a replacement home after an explicitly requested reset.
| IDE | Contest languages | Configured build and run actions |
| --- | --- | --- |
| Geany | C, C++, Java, Python | F9 builds and F5 runs. C and C++ use the GCC 15 drivers, Java uses `compilejava` and `runjava`, and Python uses PyPy. |
| Code::Blocks | C, C++ | F9 uses the default ACPC GCC 15 profile, with separate C and C++ drivers. |
| VS Code | C, C++, Java, Python | F6 runs **ACPC: Build and run** for the active file. Ctrl+Shift+B builds it. Java 25 and PyPy are configured, and the menu opens `~/contest` because tasks require an open folder. |
| CLion | C, C++, Python | The Run action uses the GCC 15 toolchain and single-file templates for C and C++. New Python projects inherit PyPy. |
| IntelliJ IDEA | Java | The Run action uses the Java 25 SDK, UTF-8 encoding and the contest Java options. |
| PyCharm | Python | New projects inherit PyPy for the Run action. |
| Eclipse | C, C++, Java, Python | The menu prepares the contest workspace. Use **ACPC Build and run** for the active source file. Java 25, PyDev with PyPy, and GCC 15 drivers are configured. |
`files/desktop/configure_ides.py` writes the IDE preferences and launch configurations. The C and C++ drivers in `files/ide/` apply the contest language standard, optimization and static linking options before calling the corresponding system compiler. The `acpc-solution` command builds the selected source and runs it only if the build succeeds. Java compilation uses `javac`, and Python syntax checking and execution use PyPy.
The ordinary Java actions in Eclipse and VS Code use the Eclipse Java compiler, also called ECJ or JDT. Use their named ACPC actions to compile with the contest's `javac` command. For imported projects, alternative workspaces or Python virtual environments, follow the compiler checks in the technical notes. The notes also describe the remaining startup dialogs and where to enter program input.
JetBrains user agreements are accepted during preparation, data sharing is declined, and `/home/team` is marked as a trusted project location. The offline AI completion models and inference programs supplied with IntelliJ IDEA and PyCharm are removed to prevent their use for generated code during the contest. Access to online AI services, including those used by editor extensions, is restricted by the website allowlist.
Language documentation is available offline at `https://devdocs/`. The local DevDocs copy includes C, C++, OpenJDK 25 and Python 3.11. Kotlin documentation and the prompt to open the external DevDocs site are removed. The C manual pages, such as `man 3 qsort`, and Python documentation through `pypy3 -m pydoc` also work offline.
The **Contest Help (offline)** bookmark opens `/usr/share/doc/acpc/help/index.html`. It contains 12 searchable articles on input and output, end of file, local testing, judging and common C, C++, Java and Python questions. These are concise summaries of 17 selected Kattis help articles, with source links and instructions for this image. The text, search and layout are stored locally, so the page needs no external scripts, fonts or help widgets. The online help portal, support tickets and status subscriptions are outside the approved contest access.
The team instructions are in `docs/TechNotes.pdf`, built from `docs/TechNotes.tex` with LuaLaTeX. The build installs the PDF at `/usr/share/doc/icpc/TechNotes.pdf`, accessible from the **Technical Notes** bookmark. After changing the source, run `lualatex TechNotes.tex` in `docs/` and rebuild the image.
## Menus and browser
The original World Finals links and PC² submission commands are replaced with the ACPC pages on Kattis. The contest and documentation entries are available from the Applications menu and Firefox bookmarks. The `submit` and `pc2submit` commands print the Kattis submission address.
| Location | Content |
| --- | --- |
| Firefox home page | The ACPC contest site |
| Firefox bookmarks | Contest, Problems, Standings, Contest Help (offline), Language Documentation, Technical Notes |
| Applications, Contest | Contest (Kattis), Problems, Standings, Contest Help (offline) |
| Applications, Documentation | Language Documentation, Technical Notes |
Firefox's home page is locked to the contest site. Extension installation is blocked to prevent contestants from adding browser tools, and access to `about:config` is blocked to prevent changes through Firefox's advanced preferences screen. The preferences needed for website filtering are also locked by policy.
The image retains `printfile` for sites that provide printing. Staff must configure the printer and the firewall rules for its address and protocol before handing over a machine. Test printing from the team account with the contest restrictions enabled.
## Clock
The system clock is synchronized by chrony, the time service. It uses the servers in `ntp_servers.txt`, with the timezone set to `America/Moncton`. At startup, the desktop waits up to about 30 seconds for synchronization. Correct time prevents misleading clock displays and browser certificate errors caused by an incorrect date.
The Ubuntu defaults use Network Time Security (NTS), which authenticates replies from time servers. NTS requires an additional connection on TCP port 4460, which this firewall does not permit. When those sources are configured but unreachable, chrony does not simply fall back to the ordinary time servers added for the contest. The installer disables Ubuntu's default source file so that chrony can use the servers in `ntp_servers.txt`. The protocol and source selection rules are described in the [Ubuntu time synchronization guide](https://ubuntu.com/server/docs/how-to/networking/chrony-client/) and the [chrony configuration manual](https://chrony-project.org/doc/4.7/chrony.conf.html#authselectmode).
If chrony still has no selected time server two minutes after boot, `time_from_contest.timer` checks once a minute and adjusts the clock from the date returned by the contest website. This provides a fallback when the campus network blocks ordinary time synchronization traffic. Chrony is configured not to copy its time to the hardware clock, to avoid changing the clock convention used by Windows on shared lab machines. Run `sudo /test_firewall.sh` to check the current synchronization status.
## Network restrictions
The firewall permits HTTPS access to the names in `allowed_domains.txt`. DNS lookups, which translate names into addresses, go through a local filter, and time synchronization is restricted to chrony. Other new outgoing connections, new unsolicited incoming connections and forwarded traffic are blocked. The base ICPC image disables IPv6.
| Control | What it is intended to prevent |
| --- | --- |
| Website allowlist applied to every program | Access to messaging, external code repositories, AI websites and other unapproved sites through a browser or command-line program |
| Local HTTPS proxy that checks the requested hostname | Access to an unapproved website merely because it shares a server address with Kattis or another allowed site |
| Local DNS filter and restricted access to DNS servers | Use of arbitrary DNS queries or an alternative resolver to communicate with an outside server |
| NTP access limited to chrony and the configured server addresses | Use of the time synchronization exception by contestant programs to contact other systems |
| Incoming and forwarding rules | Remote connections into a contest workstation and use of it to relay another device's network traffic |
| Rules applied across network interfaces | Avoiding the restrictions by sending traffic through another network connection |
| `open.kattis.com` excluded from the web allowlist and redirected locally in `/etc/hosts` | Access to the public Kattis practice site and past submissions through that hostname |
The contest site uses Cloudflare, whose [IP addresses are shared by many websites](https://developers.cloudflare.com/fundamentals/concepts/cloudflare-ip-addresses/). Allowing those addresses alone would permit connections to unrelated sites hosted at the same addresses. The local proxy therefore checks the hostname sent at the start of an encrypted connection, called TLS Server Name Indication (SNI). It accepts only a listed hostname, resolves that name itself and opens the external connection under a dedicated service account. Contestant programs cannot open external HTTPS connections directly. Firefox's Encrypted Client Hello feature is disabled so that the proxy can read the hostname.
The proxy checks the connection's hostname without decrypting the page contents. Plain HTTP requests receive a local redirect to HTTPS. The DNS filter forwards lookups for the approved web domains and time servers to the resolvers in `dns_servers.txt`. Only the proxy and DNS service account can contact those resolvers directly.
### Allowed domains and verification
The approved access covers the contest pages, Kattis password sign-in and status viewing. Teams sign in with their Kattis username or email address and password. External sign-in providers and webmail are blocked. Staff should ensure that issued team accounts have working passwords before the contest, including any password recovery that requires email.
The eight hostnames in `allowed_domains.txt` serve these pages and their supporting resources. The October 15, 2026 scan covered public contest pages, signed-in Open Kattis pages, login-related pages and service status. The table identifies where each hostname was observed during that scan.
| Domain | Service | Evidence from the October 15, 2026 scan |
| --- | --- | --- |
| `atlantic-canada26.kattis.com` | ACPC 2026 contest | Contest navigation, standings, rules, team pages and password login form |
| `status.kattis.com` | Kattis service status | Status page, history and charts |
| `1v5nt68l4ppj.statuspage.io` | Kattis status banner hosted by Atlassian | Status banner requests from Kattis pages |
| `dka575ofm4ao0.cloudfront.net` | Scripts, style sheets, fonts and images for the status page | Resources loaded by the status page |
| `js.sentry-cdn.com`, `browser.sentry-cdn.com` | Sentry browser error reporting code | Scripts loaded by public and signed-in Kattis pages |
| `challenges.cloudflare.com` | Cloudflare browser verification | Verification resources on registration and password-recovery pages |
| `licensebuttons.net` | License badges on problem pages | License badge in the Hello World problem's Metadata tab |
Each web entry is an exact hostname. The Statuspage identifier and CloudFront name stayed the same across three fresh browser sessions in the October 15 scan. File paths, version hashes and query parameters can change within those hostnames without requiring a firewall edit.
To investigate a broken contest page, use the Network tab in Firefox's developer tools, opened with F12. `sudo journalctl -t contest_proxy` lists hostnames recorded by the proxy, including refused connections. Review any proposed addition before changing `allowed_domains.txt` and rebuilding the image. If the campus requires its own DNS or time servers, put their addresses or names in `dns_servers.txt` and `ntp_servers.txt` before building.
### Connecting a workstation to Wi-Fi
Staff can connect Wi-Fi from the `icpc` administrator account while the contest restrictions remain enabled. NetworkManager saves the connection for automatic reconnection, including after a reboot. The `team` account cannot use the network controls to switch connections, edit saved profiles or turn Wi-Fi off. A USB Wi-Fi adapter should be connected before startup so that USBGuard accepts it with the other approved equipment.
For a network with a shared Wi-Fi password, open a terminal on the team desktop and run `su - icpc`. Enter the administrator password, then replace `SSID` with the approved network's name in these commands:
```
sudo nmcli radio wifi on
sudo nmcli device wifi list
sudo nmcli --ask device wifi connect "SSID" name "ACPC Wi-Fi" private no
sudo nmcli connection modify "ACPC Wi-Fi" connection.autoconnect yes connection.permissions ""
```
The `--ask` option prompts for the Wi-Fi password so it is not included in the command history. The connection is saved for the machine, and its credentials are stored in files readable only by root. Allowing the machine to reconnect does not grant the team account permission to change networks. These options are described in the [NetworkManager command reference](https://networkmanager.dev/docs/api/latest/nmcli.html).
Review saved connections with `sudo nmcli -f NAME,TYPE,AUTOCONNECT connection show`. Remove any unapproved Wi-Fi profile with `sudo nmcli connection delete "PROFILE NAME"` so that the machine cannot reconnect to it automatically. If the approved profile already exists, use `sudo nmcli connection up "ACPC Wi-Fi"` to reconnect. There is no need to run `contest-restrictions off network` for this procedure.
Before handing over the workstation, check the active connection with `nmcli connection show --active`, run `sudo contest-restrictions status` and `sudo /test_firewall.sh`, then type `exit` to leave the administrator shell. The status must show the firewall, USB and network restrictions all on. Confirm that the approved Wi-Fi reconnects after a supervised restart on the actual contest hardware.
Campus networks that require an institutional username and certificate use an enterprise Wi-Fi profile. Have campus IT provide the authentication settings, trusted CA certificate and expected authentication-server name, and configure that profile before the contest. For a network with a web sign-in page, arrange contest access with campus IT in advance so teams do not need to open that page during the contest.
## Devices and local restrictions
The device restrictions are installed by `lockdown.yml`. They prevent contestants from importing files through removable media, attaching devices that enter prepared text, or changing local settings that staff need to control. Staff can temporarily lift the USB and network restrictions with the maintenance command.
| Control | What it is intended to prevent |
| --- | --- |
| USBGuard rejects USB devices connected after startup | Importing files from a new USB drive or entering prepared code through a device that poses as a keyboard |
| Desktop authorization rules deny disk and disk-image mounting to non-administrators | Opening removable drives or disk images through the desktop's storage service |
| Device rules remove automatic access to phones, cameras and media players | Copying files through phone or camera protocols that do not require a mounted disk |
| Network and modem configuration restricted to administrators | Changing Wi-Fi, hotspot, VPN or modem settings during the contest |
| Restart, shutdown, suspend and hibernation denied to non-administrators | Interrupting the workstation or restarting it with newly attached devices |
| Ctrl+Alt+Del, SysRq shortcuts, power and sleep keys, and lid actions disabled | Triggering those power actions through keyboard shortcuts or desktop power management |
| `team` removed from the printer administration group | Changing printers or writing printer administration files while retaining ordinary printing access |
USB devices present when USBGuard starts are accepted, including the boot stick, keyboard and mouse. Staff should attach the intended equipment before startup. If a keyboard or mouse needs replacement during the contest, inspect the replacement and use `sudo usbguard list-devices` to find its number, then `sudo usbguard allow-device NUMBER` to authorize it.
## Maintenance
Use the `icpc` administrator account for maintenance. `contest-restrictions` controls the firewall, USB restrictions and network configuration permissions. Restrict the change to the part needed for the repair.
```
sudo contest-restrictions status
sudo contest-restrictions off firewall
sudo contest-restrictions on firewall
sudo contest-restrictions report
```
| Part | Effect of `off` |
| --- | --- |
| `firewall` | Allow network traffic and restore ordinary DNS resolution |
| `usb` | Stop USBGuard and allow the team account to use removable disks and phones |
| `network` | Allow the team account to change network settings |
| `all` | Apply all three changes |
Use `usb`, `network` or `all` in place of `firewall` as needed. Omitting the part applies the command to all three. A disabled restriction stays disabled after a reboot until staff explicitly enable it again. After USB maintenance, disconnect temporary devices before running `on usb`, then check the devices that remain connected.
While any of these restrictions is off, a red banner appears across the top of the desktop. It reads “CONTEST RESTRICTIONS ARE OFF ON THIS MACHINE - CALL THE STAFF”. The banner process runs as root, has no close button and is restarted if it stops. Its purpose is to alert staff before a machine is returned to a team with maintenance access still enabled.
`report` summarizes refused website connections, DNS lookups and USB devices since midnight. To choose another starting time, use a command such as `sudo contest-restrictions report "2026-10-23 12:00"`. Common background lookups from Firefox and the IDEs are counted separately. Use the report to investigate an incident or a page that failed to load, with the corresponding service logs for details.
The commands `/disable_firewall.sh` and `/enable_firewall.sh` are the firewall helpers called by `contest-restrictions off firewall` and `contest-restrictions on firewall`. They control the same firewall and preserve its state across reboots. Use `contest-restrictions` on the full image for a consistent maintenance procedure. The separate helpers remain available for installations that include only the firewall.
### Manual deletion of the team's files
> Running `sudo /reset_team_home.sh` permanently deletes the team's work on that machine. It stops the desktop session, so unsaved work in open applications is lost too. The script makes no backup and offers no undo.
This command is reserved for a deliberate staff decision to erase a team account. It is never started by boot, login, a timer, cron, the build playbooks or `contest-restrictions`. Do not add it to an automatic cleanup procedure, maintenance sequence or automated test. Routine troubleshooting, changing a restriction and rebooting a machine do not require a reset.
Before using it, identify the machine and team, obtain authorization from the staff member responsible for their work, and save and verify a copy of every file that must be retained. Check `/tmp`, `/var/tmp` and any other work directories as well as the home directory. The deletion includes `/home/team` and other files owned by `team` on the root filesystem. Treat team-owned temporary files, the team's crontab and any other team-owned work directory on that filesystem as part of the deletion. The script then recreates the home from `/etc/skel` and starts a new team desktop.
From an administrator terminal, run `sudo /reset_team_home.sh` only after those checks. The script asks for the exact word `RESET` before proceeding. After confirmation, a separate system service continues the deletion even when the terminal closes or the desktop disappears. Starting it through `su - icpc` in a team terminal has the same consequences and will terminate that desktop session. Treat confirmation as the point after which the team's files will be erased.
## Onsite supervision checklist
These checks are for the staff supervising contestants and their workstations.
- [ ] Keep the prepared contest sticks under staff control until the workstations are ready for the teams.
- [ ] Supervise startup and any restart. Check that no unauthorized USB device is connected during boot. Confirm that the machine starts from the supplied contest stick, and have staff handle device replacements.
- [ ] Check that each team uses its assigned workstation, the `team` desktop account and its issued Kattis contest account.
- [ ] Confirm that each workstation uses the approved wired or Wi-Fi connection. Refer connection problems to technical staff instead of allowing teams to use personal hotspots.
- [ ] Keep administrator credentials private. Close administrator terminals and sessions before returning a workstation to a team.
- [ ] If the red restrictions banner appears, call technical staff and keep the workstation out of use until the restrictions are restored.
- [ ] Supervise maintenance and remove temporary devices afterward. Confirm that the warning banner has disappeared before the team resumes work.
- [ ] Report unexpected access to websites, personal accounts or devices to the contest organizers, with the workstation and time concerned.
## Installing only the firewall
On a machine already running the ICPC image, staff can install the firewall separately. This installs the network filter, DNS filter, proxy, time configuration and firewall administration commands. Accounts, boot protection, USB restrictions and the desktop warning banner are configured by the full image build. For a separate installation, the site is responsible for those controls. From a local administrator console in a copy of this repository, run:
```
sudo ./install_firewall.sh --start
```
The installer needs Internet access to obtain `nftables`, `dnsutils`, `chrony`, `nginx`, `libnginx-mod-stream` and `dnsmasq-base`. With `--start`, the firewall becomes active during the installation and rejects new incoming connections, including SSH. Without that option, it is enabled for the next boot and can also be started with `/enable_firewall.sh`.
Both installation methods, the shell installer and `firewall.yml`, install the following commands directly in the filesystem root. They remain available even when `contest-restrictions` is not installed. Use them from the machine's administrator account:
```
sudo /disable_firewall.sh
sudo /enable_firewall.sh
sudo /test_firewall.sh
```
The disable command allows network traffic and restores ordinary DNS resolution until the enable command is run, including after reboots. The enable command starts the firewall, proxy, DNS filter and address refresh services, and enables the firewall for subsequent boots. The test command reports allowed and blocked website access, local DevDocs access, DNS filtering, proxy status and clock synchronization. It does not install or toggle the firewall.
For installation with Ansible over SSH, replace `HOST` with the machine's address and `ADMIN_USER` with an account that has sudo access. Run this from the repository on the administrator's computer. The target must be reachable by SSH before starting:
```
ansible-playbook -i "HOST," -u ADMIN_USER --ask-pass -K -e ansible_become_exe=sudo.ws firewall.yml
```
The `sudo.ws` setting selects the sudo implementation whose password prompt Ansible recognizes on this image. The firewall starts at the end of the playbook, after which new SSH connections are refused. Add `-e start_firewall=false` to defer activation until the next boot or a local enable command. The same enable, disable and test scripts are installed in either case.
## Testing
The procedures in `tests/README.md` use a disposable virtual machine to check the image as a team member and an administrator would use it. They include service checks, desktop interaction and compilation in every supported IDE and language. Graphical reports are generated locally in `test_report/`, with the IDE compilation report at `test_report/ide-config/index.html`.
## Files
| File | Purpose |
| --- | --- |
| `build.sh` | Build and configure the contest disk image |
| `run_vm.sh` | Open a disposable session for inspecting the built image |
| `write_sticks.sh` | Overwrite the selected USB disks with the built image |
| `site.yml` | Apply the account, language, browser, desktop, documentation, firewall and device playbooks, then finish the build |
| `finish.yml` | Restore the SSH restrictions, set passwords, remove team sudo membership and shut down the build VM |
| `allowed_domains.txt`, `dns_servers.txt`, `ntp_servers.txt` | Network lists copied to `/etc/contest-firewall/` |
| `install_firewall.sh` | Install the firewall on the running ICPC machine with shell commands, without Ansible |
| `firewall.yml` | Install the same firewall through Ansible |
| `test_firewall.sh` | Check an installed firewall. Available as `/test_firewall.sh` on the contest machine |
| `files/enable_firewall.sh`, `files/disable_firewall.sh` | Installed as `/enable_firewall.sh` and `/disable_firewall.sh` by both firewall installers |
| `files/help/index.html`, `docs.yml` | Offline contest help and its installation alongside the local language documentation |
| `files/` | Scripts, configuration and system services installed by the playbooks |
| `docs/` | Technical notes for contestants |
| `tests/` | Tools and instructions for checking a disposable copy of the image |
+111
View File
@@ -0,0 +1,111 @@
- name: Accounts (team for the teams, icpc for the administrator)
hosts: all
become: yes
vars:
# admin_pass comes from build.sh. team_pass is optional: without it, the
# team account keeps the password of the ICPC image (contest).
team_pass: ""
autologin: true
tasks:
# The password of icpc changes and team leaves the sudo group in finish.yml,
# at the very end, because Ansible may be using those accounts for sudo
- name: Check that the administrator password is given
assert:
that: admin_pass is defined and admin_pass | length > 0
fail_msg: "Give the new password of the icpc account with -e admin_pass=..."
- name: Make sure the root account stays locked (no root password)
command: passwd -l root
register: lock_root
changed_when: "'unchanged' not in lock_root.stdout"
- name: Log the teams in automatically (team account)
copy:
dest: /etc/lightdm/lightdm.conf.d/90-acpc-autologin.conf
mode: '0644'
content: |
[Seat:*]
autologin-user=team
autologin-user-timeout=0
when: autologin | bool
- name: No automatic login
file:
path: /etc/lightdm/lightdm.conf.d/90-acpc-autologin.conf
state: absent
when: not (autologin | bool)
# The ICPC image lets every account run vscode-extension-install as root
# without a password, at login. That script mounts and changes paths in
# the home directory, which the account controls. Here it runs at boot for
# the team account only, after a check of those paths.
- name: Remove the sudo rule of vscode-extension-install
file:
path: /etc/sudoers.d/vscode-extension-install
state: absent
- name: Do not run vscode-extension-install at login
lineinfile:
path: /etc/xdg/autostart/vscode-extension-install.desktop
line: Hidden=true
- name: Install the boot-time VS Code setup of the team account
copy:
src: "files/security/{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
loop:
- { src: team-vscode-extensions, dest: /usr/local/sbin/team-vscode-extensions, mode: '0700' }
- { src: team-vscode-extensions.service, dest: /etc/systemd/system/team-vscode-extensions.service, mode: '0644' }
- name: Enable it
systemd:
name: team-vscode-extensions.service
enabled: yes
daemon_reload: yes
# The boot menu is hidden, but Esc shows it, and editing an entry gives a
# root shell (init=/bin/bash). Editing and the other entries need the user
# admin with the administrator password. The normal boot needs nothing.
- name: Hash the boot menu password
shell: printf '%s\n%s\n' "$PASS" "$PASS" | grub-mkpasswd-pbkdf2 | sed -n 's/.* is //p'
environment:
PASS: "{{ admin_pass }}"
register: grub_hash
changed_when: false
no_log: true
- name: Protect the boot menu with a password
copy:
dest: /etc/grub.d/01_acpc_password
# Root only: the file holds the password hash (also the icpc password)
mode: '0700'
content: |
#!/bin/sh
cat <<'EOF'
set superusers="admin"
password_pbkdf2 admin {{ grub_hash.stdout }}
EOF
no_log: true
- name: Let the Ubuntu entries boot without the password
replace:
path: /etc/grub.d/10_linux
regexp: '^CLASS="--class gnu-linux --class gnu --class os"$'
replace: 'CLASS="--class gnu-linux --class gnu --class os --unrestricted"'
- name: No recovery entries in the boot menu
copy:
dest: /etc/default/grub.d/90-acpc.cfg
mode: '0644'
content: |
GRUB_DISABLE_RECOVERY="true"
- name: Rebuild the boot menu
command: update-grub
changed_when: true
- name: Check the boot menu
shell: grep -c -- '--unrestricted' /boot/grub/grub.cfg && grep -q 'set superusers="admin"' /boot/grub/grub.cfg
changed_when: false
+22
View File
@@ -0,0 +1,22 @@
# Web access (HTTPS) is allowed only to these domains.
# One domain per line. Lines starting with # are comments.
# The local proxy checks the site name of each connection against this list.
# Contest site (Kattis)
atlantic-canada26.kattis.com
# Kattis service status and the embedded status banner
status.kattis.com
1v5nt68l4ppj.statuspage.io
# Status-page scripts, styles, fonts and images (one CloudFront name only)
dka575ofm4ao0.cloudfront.net
# Browser error-reporting libraries loaded by the Kattis pages
js.sentry-cdn.com
browser.sentry-cdn.com
# Cloudflare challenge, shown if Cloudflare decides to check the browser
challenges.cloudflare.com
# Licence badges on the problem pages
licensebuttons.net
Executable
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
# Builds the ACPC 2026 contest image from the official ICPC image.
#
# 1. Download the ICPC image (streamed: checksum, decompression and a
# sparse raw file in one pass, the .gz is not kept).
# 2. Create build/acpc2026.qcow2, an overlay on top of the ICPC image.
# The ICPC image itself is never modified.
# 3. Allow SSH password login in the overlay, offline (debugfs through a
# qemu-storage-daemon FUSE export, no root needed).
# 4. Boot the overlay in QEMU/KVM and configure it with Ansible (site.yml).
# 5. Power off. The result is written to USB sticks with write_sticks.sh,
# or exported to a raw image with --export.
#
# Usage: ./build.sh --admin-pass PASSWORD [options] (./build.sh -h for the list)
set -euo pipefail
IMG_URL=${IMG_URL:-https://image.icpc.global/icpc2027/icpc2027-2026-09-29_image-amd64.img.gz}
IMG_SHA256=${IMG_SHA256:-e81f1cb46abd19782c2bcbccbad2dca63a198650b38ba5b4f7b54f3f29fd1045}
BASE_IMG=${BASE_IMG:-}
ADMIN_PASS=${ADMIN_PASS:-}
TEAM_PASS=${TEAM_PASS:-}
STOCK_PASS=contest # password of the team account in the ICPC image
RAM=${RAM:-4096}
CPUS=${CPUS:-4}
PORT_SSH=${PORT_SSH:-2222}
FRESH=0
EXPORT=0
usage() {
cat <<'EOF'
Usage: ./build.sh --admin-pass PASSWORD [options]
--admin-pass PASS New password of the administrator account (icpc), also
asked to edit the boot menu (user admin). Required.
--team-pass PASS New password of the team account, used by the teams.
Default: unchanged (contest, as in the ICPC image).
--base-image FILE Use an ICPC image already decompressed (raw .img)
instead of downloading it.
--fresh Start again from the ICPC image (drops build/acpc2026.qcow2).
--export After the build, also write build/acpc2026.img (raw,
sparse), for tools that cannot read qcow2.
-h, --help Show this help.
Environment: IMG_URL, IMG_SHA256, RAM (MB), CPUS, PORT_SSH.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--admin-pass) ADMIN_PASS=$2; shift 2 ;;
--team-pass) TEAM_PASS=$2; shift 2 ;;
--base-image) BASE_IMG=$2; shift 2 ;;
--fresh) FRESH=1; shift ;;
--export) EXPORT=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
cd "$(dirname "$0")"
ROOT=$(pwd)
BUILD=$ROOT/build
mkdir -p "$BUILD"
[ -n "$BASE_IMG" ] || BASE_IMG=$BUILD/$(basename "${IMG_URL%.gz}")
BASE_IMG=$(realpath -m "$BASE_IMG")
OVERLAY=$BUILD/acpc2026.qcow2
VARS=$BUILD/OVMF_VARS.fd
MON=$BUILD/qemu-monitor.sock
FUSE=$BUILD/fuse.raw
log() { printf '\033[1;36m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; }
fail() { printf '\033[1;31m[%s] ERROR\033[0m %s\n' "$(date +%H:%M:%S)" "$*" >&2; exit 1; }
[ -n "$ADMIN_PASS" ] || { usage >&2; fail "--admin-pass is required"; }
for tool in qemu-system-x86_64 qemu-img qemu-storage-daemon fusermount3 debugfs \
sfdisk ansible-playbook sshpass curl python3; do
command -v "$tool" >/dev/null || fail "missing tool: $tool"
done
[ -w /dev/kvm ] || fail "/dev/kvm is not available or not writable"
OVMF_CODE=""
for dir in /usr/share/edk2/x64 /usr/share/OVMF /usr/share/edk2/ovmf; do
for code in OVMF_CODE.4m.fd OVMF_CODE_4M.fd OVMF_CODE.fd; do
if [ -f "$dir/$code" ]; then
OVMF_CODE=$dir/$code
OVMF_VARS=$dir/${code/CODE/VARS}
break 2
fi
done
done
[ -n "$OVMF_CODE" ] && [ -f "$OVMF_VARS" ] || fail "UEFI firmware (OVMF) not found, install edk2-ovmf"
vm_running() { pgrep -f "^qemu-system-x86_64 -name acpc2026-build" >/dev/null; }
monitor() {
python3 -I - "$MON" "$1" <<'PY'
import socket, sys, time
s = socket.socket(socket.AF_UNIX)
s.connect(sys.argv[1])
time.sleep(0.2)
s.sendall((sys.argv[2] + "\n").encode())
time.sleep(0.5)
PY
}
stop_vm() {
vm_running || return 0
monitor system_powerdown 2>/dev/null || true
for _ in $(seq 1 100); do vm_running || return 0; sleep 3; done
monitor quit 2>/dev/null || true
sleep 2
}
cleanup() {
stop_vm
if mountpoint -q "$FUSE" 2>/dev/null; then fusermount3 -u "$FUSE"; fi
pkill -f "^qemu-storage-daemon .*$OVERLAY" 2>/dev/null || true
rm -f "$BUILD/vars.yml"
}
trap cleanup EXIT
# --- 1. ICPC image ------------------------------------------------------------
if [ -f "$BASE_IMG" ]; then
log "1. ICPC image already present: $BASE_IMG"
else
log "1. Downloading $IMG_URL"
curl -fL --retry 5 "$IMG_URL" \
| tee >(sha256sum | awk '{print $1}' > "$BASE_IMG.sha256") \
| gzip -dc | dd of="$BASE_IMG.part" bs=4M conv=sparse status=none
sleep 1
got=$(cat "$BASE_IMG.sha256")
[ "$got" = "$IMG_SHA256" ] || fail "SHA-256 mismatch: got $got, expected $IMG_SHA256"
mv "$BASE_IMG.part" "$BASE_IMG"
log " SHA-256 OK"
fi
# --- 2. Overlay ---------------------------------------------------------------
if [ $FRESH -eq 1 ] || [ ! -f "$OVERLAY" ]; then
log "2. Creating the overlay $OVERLAY"
qemu-img create -q -f qcow2 -b "$BASE_IMG" -F raw "$OVERLAY"
cp "$OVMF_VARS" "$VARS"
else
log "2. Reusing the overlay $OVERLAY (--fresh to start again)"
[ -f "$VARS" ] || cp "$OVMF_VARS" "$VARS"
fi
# --- 3. Offline changes for the build -----------------------------------------
# The ICPC image refuses SSH password logins and denies the group "teams", to
# which the team account belongs. For the build, icpc.conf is removed and
# 00-build.conf allows password logins. finish.yml puts icpc.conf back. The
# firewall is turned off for the boot, in case the overlay was already built
# (site.yml turns it back on).
log "3. Allowing SSH password login in the overlay"
touch "$FUSE"
qemu-storage-daemon \
--blockdev driver=file,node-name=file,filename="$OVERLAY" \
--blockdev driver=qcow2,node-name=disk,file=file \
--export type=fuse,id=exp,node-name=disk,mountpoint="$FUSE",writable=on \
> "$BUILD/qsd.log" 2>&1 &
for _ in $(seq 1 20); do mountpoint -q "$FUSE" && break; sleep 0.5; done
mountpoint -q "$FUSE" || fail "qemu-storage-daemon did not start (see $BUILD/qsd.log)"
# The root file system is the largest partition
start=$(sfdisk -J "$FUSE" | python3 -I -c '
import json, sys
parts = json.load(sys.stdin)["partitiontable"]["partitions"]
print(max(parts, key=lambda p: p["size"])["start"])')
DEV="$FUSE?offset=$((start * 512))"
printf 'PasswordAuthentication yes\nKbdInteractiveAuthentication yes\nPermitRootLogin no\n' > "$BUILD/00-build.conf"
dfs() { debugfs -w -R "$1" "$DEV" >/dev/null 2>&1 || true; }
dfs "rm /etc/ssh/sshd_config.d/icpc.conf"
dfs "rm /etc/ssh/sshd_config.d/00-build.conf"
dfs "write $BUILD/00-build.conf /etc/ssh/sshd_config.d/00-build.conf"
for field in "mode 0100644" "uid 0" "gid 0"; do
dfs "set_inode_field /etc/ssh/sshd_config.d/00-build.conf $field"
done
dfs "rm /etc/systemd/system/multi-user.target.wants/firewall.service"
debugfs -R "ls /etc/ssh/sshd_config.d" "$DEV" 2>/dev/null | grep -q 00-build.conf \
|| fail "could not write 00-build.conf in the image"
fusermount3 -u "$FUSE"
pkill -f "^qemu-storage-daemon .*$OVERLAY" || true
sleep 2
rm -f "$FUSE" "$BUILD/00-build.conf"
# --- 4. Boot and configure ----------------------------------------------------
log "4. Booting the image (SSH on 127.0.0.1:$PORT_SSH)"
vm_running && fail "a build VM is already running"
qemu-system-x86_64 -name acpc2026-build -machine q35,accel=kvm -cpu host \
-m "$RAM" -smp "$CPUS" \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$VARS" \
-drive id=disk0,file="$OVERLAY",format=qcow2,if=none \
-device virtio-blk-pci,drive=disk0,bootindex=1 \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$PORT_SSH"-:22 \
-device virtio-net-pci,netdev=net0 \
-display none -monitor unix:"$MON",server,nowait \
-serial file:"$BUILD/serial.log" &
ssh_ok() {
sshpass -p "$2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -o ConnectTimeout=5 -o PreferredAuthentications=password \
-p "$PORT_SSH" "$1@127.0.0.1" 'sudo -n true 2>/dev/null; id -nG' 2>/dev/null | grep -qw sudo
}
# First build: the team account of the ICPC image (password contest, sudo).
# Next builds: the icpc account with the new password (team lost sudo).
LOGIN_USER=""
for _ in $(seq 1 120); do
vm_running || fail "QEMU stopped (see $BUILD/serial.log)"
if ssh_ok icpc "$ADMIN_PASS"; then LOGIN_USER=icpc; LOGIN_PASS=$ADMIN_PASS; break; fi
if ssh_ok team "$STOCK_PASS"; then LOGIN_USER=team; LOGIN_PASS=$STOCK_PASS; break; fi
sleep 5
done
[ -n "$LOGIN_USER" ] || fail "no SSH login with sudo after 10 minutes (icpc with --admin-pass, or team/contest)"
log " SSH login OK ($LOGIN_USER)"
# Passwords go through a file readable only by the user, not the command line
( umask 077; python3 -I - "$BUILD/vars.yml" "$LOGIN_PASS" "$ADMIN_PASS" "$TEAM_PASS" <<'PY'
import json, sys
path, login, admin, team = sys.argv[1:]
json.dump({"ansible_password": login, "ansible_become_password": login,
"admin_pass": admin, "team_pass": team}, open(path, "w"))
PY
)
log " Running site.yml"
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i "127.0.0.1," -u "$LOGIN_USER" \
-e ansible_port="$PORT_SSH" -e ansible_become_exe=sudo.ws \
-e "@$BUILD/vars.yml" -e start_firewall=false site.yml
rm -f "$BUILD/vars.yml"
# --- 5. Power off -------------------------------------------------------------
# finish.yml powers the machine off as its last task
log "5. Waiting for the image to power off"
for _ in $(seq 1 200); do vm_running || break; sleep 3; done
stop_vm
vm_running && fail "the VM did not power off"
if [ $EXPORT -eq 1 ]; then
log " Exporting $BUILD/acpc2026.img"
qemu-img convert -p -O raw "$OVERLAY" "$BUILD/acpc2026.img"
fi
cat <<EOF
Image ready: $OVERLAY
Teams: team (automatic login, no sudo), password ${TEAM_PASS:-contest}
Administrator: icpc (sudo), password given with --admin-pass
Boot menu: user admin, same password
Write it to USB sticks with: sudo ./write_sticks.sh $OVERLAY
Try it in a window with: ./run_vm.sh
EOF
+146
View File
@@ -0,0 +1,146 @@
- name: Menus and commands of the contest (ACPC 2026 on Kattis)
hosts: all
become: yes
vars:
contest_url: https://atlantic-canada26.kattis.com
contest_id: atlantic-canada26
# Contest menu (category "Contest" of the Xubuntu menu of the ICPC image)
contest_entries:
- { file: acpc-contest, name: "Contest (Kattis)", url: "{{ contest_url }}/contests/{{ contest_id }}" }
- { file: acpc-problems, name: "Problems", url: "{{ contest_url }}/contests/{{ contest_id }}/problems" }
- { file: acpc-standings, name: "Standings", url: "{{ contest_url }}/contests/{{ contest_id }}/standings" }
- { file: acpc-help, name: "Contest Help (offline)", url: "file:///usr/share/doc/acpc/help/index.html" }
tasks:
# Entries of the ICPC image made for the World Finals: their contest
# server (https://contest), the PC² guide and the World Finals judging notes
- name: Remove the menu entries that do not apply to this contest
file:
path: "/usr/share/applications/{{ item }}"
state: absent
loop:
- ccs.desktop
- scoreboard.desktop
- problems.desktop
- ccsdocs.desktop
- judgingnotes.desktop
- name: Contest menu with the Kattis pages
copy:
dest: "/usr/share/applications/{{ item.file }}.desktop"
mode: '0644'
content: |
[Desktop Entry]
Type=Application
Name={{ item.name }}
Exec=firefox {{ item.url }}
Icon=firefox
Categories=Contest;
loop: "{{ contest_entries }}"
- name: Same name for the documentation as the bookmark
lineinfile:
path: /usr/share/applications/devdocs.desktop
regexp: '^Name='
line: Name=Language Documentation
# submit and pc2submit send to a PC² or DOMjudge server, which this
# contest does not use
- name: Remove the submit link of the ICPC image (it points to pc2submit)
file:
path: /usr/local/bin/submit
state: absent
- name: Replace the submit commands with a pointer to Kattis
copy:
dest: "/usr/local/bin/{{ item }}"
mode: '0755'
content: |
#!/bin/bash
echo "This contest runs on Kattis. Submit your solutions on the contest site:"
echo " {{ contest_url }}/contests/{{ contest_id }}/problems"
echo "(Submit button on the page of each problem)"
exit 1
loop:
- submit
- pc2submit
# Kotlin is not a language of this contest (the compiler stays installed)
- name: Remove the Kotlin compile and run commands
file:
path: "/usr/local/bin/{{ item }}"
state: absent
loop:
- compilekotlin
- runkotlin
# JetBrains IDEs (CLion, IntelliJ IDEA, PyCharm): first-launch questions
# answered in advance (agreement accepted by the organizers, no data
# sharing), /home/team trusted, PyPy as the Python of PyCharm. /etc/skel
# is used by reset_team_home.sh.
- name: Settings of the JetBrains IDEs for the team account
script: "files/desktop/jetbrains_first_run.sh {{ item }}"
loop:
- /home/team
- /etc/skel
# Eclipse runs programs with the Java it runs on. The ICPC image starts it
# with its own Java 21, and Kattis uses Java 25.0.4.1.
- name: Eclipse runs on the Java of the system (25.0.4.1, as on Kattis)
replace:
path: /opt/eclipse/eclipse.ini
regexp: '^plugins/org\.eclipse\.justj\.openjdk\.[^\n]*/bin/java$'
replace: /usr/lib/jvm/java-25-openjdk-amd64/bin/java
- name: Install the contest compiler drivers and IDE build command
copy:
src: "files/ide/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
mode: '0755'
loop:
- acpc-cc
- acpc-solution
- acpc-eclipse
- name: C and C++ compiler driver names
file:
src: /usr/local/bin/acpc-cc
dest: "/usr/local/bin/{{ item }}"
state: link
loop:
- acpc-gcc
- acpc-g++
- name: Eclipse CDT compiler command directory
file:
path: /usr/local/lib/acpc-toolchain
state: directory
mode: '0755'
- name: Compiler names used by Eclipse CDT GNU projects
file:
src: "/usr/local/bin/{{ item.target }}"
dest: "/usr/local/lib/acpc-toolchain/{{ item.name }}"
state: link
loop:
- { name: gcc, target: acpc-gcc }
- { name: cc, target: acpc-gcc }
- { name: g++, target: acpc-g++ }
- { name: c++, target: acpc-g++ }
- name: Eclipse menu opens the configured contest workspace
lineinfile:
path: /usr/share/applications/Eclipse.desktop
regexp: '^Exec='
line: 'Exec=/usr/local/bin/acpc-eclipse'
- name: VS Code menu opens the contest folder so its build tasks are available
replace:
path: /usr/share/applications/com.microsoft.VSCode.desktop
regexp: '^Exec=/usr/share/code/code %F$'
replace: 'Exec=/usr/share/code/code /home/team/contest %F'
- name: Configure the contest compilers in every IDE
script: files/desktop/configure_ides.py
register: ide_settings
changed_when: "'changed=0' not in ide_settings.stdout"
+4
View File
@@ -0,0 +1,4 @@
# DNS resolvers the contest machines may query (UDP and TCP port 53).
# Replace them with your campus resolvers if your network blocks external DNS.
1.1.1.1
8.8.8.8
+97
View File
@@ -0,0 +1,97 @@
- name: Local documentation (DevDocs) limited to the languages of the contest
hosts: all
become: yes
vars:
# Documentation sets of the ICPC image: c, cpp, openjdk~25, python~3.11
# and kotlin~2.4. Kotlin is not a language of this contest.
contest_docs: [c, cpp, openjdk~25, python~3.11]
remove_docs: [kotlin~2.4]
image: localhost/devdocs:latest
tasks:
- name: Create the directory for offline contest help
file:
path: /usr/share/doc/acpc/help
state: directory
owner: root
group: root
mode: '0755'
- name: Install the approved offline help summaries
copy:
src: files/help/index.html
dest: /usr/share/doc/acpc/help/index.html
owner: root
group: root
mode: '0644'
- name: List the documentation sets of the DevDocs image
command: podman run --rm --entrypoint ls {{ image }} public/docs
register: docsets
changed_when: false
- name: Check that the contest languages are all there
assert:
that: item in docsets.stdout_lines
fail_msg: "DevDocs has no documentation for {{ item }}"
loop: "{{ contest_docs }}"
# DevDocs runs in a container created from the image at every boot, so
# the change is saved into the image (podman commit)
- name: Remove the other languages from the DevDocs image
when: remove_docs | intersect(docsets.stdout_lines) | length > 0
block:
- name: Stop the DevDocs service
systemd:
name: container-devdocs.service
state: stopped
- name: Start a container to change the image
shell: |
podman rm -f devdocs-build 2>/dev/null || true
podman run -d --name devdocs-build {{ image }}
- name: Remove the documentation sets and set the default list
shell: |
podman exec devdocs-build sh -c '
for slug in {{ remove_docs | join(" ") }}; do rm -rf "public/docs/$slug"; done
ruby -rjson -e "
f = %q{public/docs/docs.json}
data = JSON.parse(File.read(f))
data.reject! { |d| %w({{ remove_docs | join(" ") }}).include?(d[%q{slug}]) }
File.write(f, JSON.generate(data))"
sed -i "s|set :default_docs, %w(.*)|set :default_docs, %w({{ contest_docs | join(" ") }})|" lib/app.rb
grep -n "set :default_docs" lib/app.rb
# The compiled assets also hold the list of documentation sets
cd public/assets
ruby -rjson -e "
f = Dir[%q{docs-*.json}].first
data = JSON.parse(File.read(f))
data.reject! { |d| %w({{ remove_docs | join(" ") }}).include?(d[%q{slug}]) }
File.write(f, JSON.pretty_generate(data))
File.write(Dir[%q{docs-*.js}].reject { |x| x.end_with?(%q{.gz}) }.first, %q{app.DOCS=} + JSON.generate(data) + %q{;})"
# DevDocs warns when it is not served from devdocs.io (here https://devdocs/)
sed -i "s|production_host:\"devdocs.io\"|production_host:\"devdocs\"|" application-*.js
for f in docs-*.json docs-*.js application-*.js; do
case "$f" in *.gz) ;; *) gzip -kf "$f" ;; esac
done
grep -c kotlin docs-*.js docs-*.json | grep -v ".gz"'
- name: Save the change into the image
shell: |
podman stop devdocs-build
podman commit devdocs-build {{ image }}
podman rm -f devdocs-build
podman image prune -f
- name: Start the DevDocs service again
systemd:
name: container-devdocs.service
state: started
- name: Check the documentation sets of the image
command: podman run --rm --entrypoint ls {{ image }} public/docs
register: docsets_after
changed_when: false
failed_when: docsets_after.stdout_lines | difference(contest_docs + ['docs.json']) | length > 0
Binary file not shown.
+222
View File
@@ -0,0 +1,222 @@
\documentclass[11pt]{article}
\usepackage{fontspec}
\usepackage[margin=2.2cm]{geometry}
\usepackage{booktabs}
\usepackage{tabularx}
\usepackage{enumitem}
\usepackage{fancyhdr}
\usepackage[hidelinks]{hyperref}
\setmainfont{TeX Gyre Heros}
\setmonofont{TeX Gyre Cursor}[Scale=1.05]
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt}
\setlist{itemsep=2pt,topsep=2pt}
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\fancyfoot[L]{\small ACPC 2026 Technical Notes}
\fancyfoot[R]{\small Page \thepage}
\newcommand{\cmd}[1]{\texttt{#1}}
\begin{document}
{\LARGE\bfseries Technical Notes}\par\vspace{2pt}
{\large Atlantic Canadian Programming Competition 2026}\par
\vspace{8pt}
Please read these notes before the contest.
They describe the contest system, the languages and the commands available on your machine.
\section*{Contest system}
The contest runs on Kattis, at \url{https://atlantic-canada26.kattis.com}.
Firefox opens on the contest page. The bookmarks bar and the Contest menu of the Applications menu lead to the contest, the problems and the standings.
Log in with your team's Kattis username or email address and password, then submit your solutions from the page of each problem.
External sign-in providers and webmail are unavailable on the contest machines. Ask staff if you cannot use the team's password.
The sample input and output of each problem are on its page.
The contest starts on Friday, October 23, 2026 at 13:00 Atlantic time and lasts 5 hours.
The clock of your machine is synchronized, so it shows the same time as Kattis.
\section*{All programs}
\begin{itemize}
\item Your program reads its input from standard input and writes its output to standard output.
Output sent to standard error is ignored, but a large amount of it can slow your program down.
\item A program that ends with a non-zero exit code is judged as a run time error.
\item The memory limit and the time limit are given on the page of each problem.
\end{itemize}
\section*{Languages}
The languages of the contest are C, C++, Java and Python~3.
The compilers on your machine are the same versions as on Kattis.
\smallskip
\begin{tabularx}{\linewidth}{@{}l l X@{}}
\toprule
Language & Version & File extensions \\
\midrule
C & gcc-15 15.2.0 & \cmd{.c} \\
C++ & g++-15 15.2.0 & \cmd{.cpp}, \cmd{.cc}, \cmd{.C}, \cmd{.cxx}, \cmd{.c++} \\
Java & javac and java 25.0.4.1 & \cmd{.java} \\
Python 3 & PyPy 7.3.23 (Python 3.11.15) & \cmd{.py}, \cmd{.py3} \\
\bottomrule
\end{tabularx}
\section*{Compiling and running on your machine}
You can test your programs any way you like.
The commands below use the same compiler options as Kattis, so they are the closest match to the way your submissions are judged.
They do not enforce the time and memory limits.
\subsection*{C}
\begin{tabular}{@{}ll@{}}
Compile & \cmd{compilec prog.c} \\
Run & \cmd{runc < input.txt} \\
\end{tabular}
\cmd{compilec} runs \cmd{gcc-15 -g -O2 -o a.out -std=gnu23 -static prog.c -lm}, which is the command of Kattis.
\cmd{runc} runs \cmd{./a.out}.
\cmd{compilegcc} is another name for \cmd{compilec}.
\subsection*{C++}
\begin{tabular}{@{}ll@{}}
Compile & \cmd{compilecpp prog.cpp} \\
Run & \cmd{runcpp < input.txt} \\
\end{tabular}
\cmd{compilecpp} runs the same command as Kattis.\\
\cmd{g++-15 -g -O2 -o a.out -std=gnu++23 -static -lrt}\\
\cmd{\ \ -Wl,--whole-archive -lpthread -Wl,--no-whole-archive prog.cpp}\\
\cmd{runcpp} runs \cmd{./a.out}.
\cmd{compileg++} is another name for \cmd{compilecpp}.
\subsection*{Java}
\begin{tabular}{@{}ll@{}}
Compile & \cmd{compilejava Prog.java} \\
Run & \cmd{runjava Prog < input.txt} \\
\end{tabular}
\cmd{compilejava} runs the same command as Kattis.\\
\cmd{javac -encoding UTF-8 -sourcepath . -cp "./*" -d . Prog.java}\\
\cmd{runjava} takes the name of the class that contains \cmd{main} and runs\\
\cmd{java -Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m}\\
\cmd{\ \ -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2}\\
\cmd{\ \ -Xms2048m -Xmx2048m -cp ".:./*" Prog}
Kattis sets \cmd{-Xms} and \cmd{-Xmx} to the memory limit of the problem.
\cmd{runjava} uses 2048~MB.
To test with another limit, for instance 1024~MB, run\\
\cmd{MEMLIM=1024 runjava Prog}
We suggest not using a \cmd{package} statement.
\subsection*{Python 3}
\begin{tabular}{@{}ll@{}}
Check the syntax & \cmd{compilepython3 prog.py} \\
Run & \cmd{runpython3 prog.py < input.txt} \\
\end{tabular}
Kattis runs Python~3 programs with PyPy (\cmd{pypy3 prog.py}), not with the usual Python interpreter.
\cmd{runpython3} runs \cmd{pypy3 prog.py} in the same way.
The \cmd{python3} command of the machine is a different interpreter (CPython 3.14) and does not match Kattis.
\cmd{compilepython3} only checks the syntax (\cmd{pypy3 -m py\_compile prog.py}).
\clearpage
\section*{Running programs from an IDE}
The contest compilers and interpreters are already configured in the IDEs below.
The actions described here use the language versions and options on the previous pages.
They do not enforce the problem's time or memory limits.
Save your source file before building, then type input in the terminal or Console/Run panel that opens.
For Java, give the public class the same name as the file and leave out the \cmd{package} statement.
Only submit source files to Kattis.
\subsection*{Geany}
You can use Geany for C, C++, Java and Python~3.
Press F9 to build your program or check its syntax, then F5 to run it in a terminal.
For C and C++, F8 compiles without linking, so use F9 before F5.
\subsection*{Code::Blocks}
Open your C or C++ source file and press F9 to build and run it.
The default ACPC GCC 15 profile uses the appropriate compiler for each language.
If the compiler-detection window appears on the first launch, confirm with \textbf{OK}.
\subsection*{VS Code}
You can use VS Code for C, C++, Java and Python~3.
Open VS Code from the Applications menu to work in \cmd{\textasciitilde/contest}, or open your own source folder.
If VS Code asks whether you trust your own folder, choose \textbf{Yes}.
Select your source file in the editor and press F6 for \textbf{ACPC: Build and run}.
Type input in the Terminal panel. Ctrl+Shift+B only builds the program.
For Java, use F6 to compile with \cmd{javac} and the Kattis options.
The ordinary Java Run action uses Eclipse's Java compiler instead.
\subsection*{CLion}
You can use CLion for C, C++ and Python~3.
Open your source folder, then your file, and wait for indexing to finish.
For C and C++, use the green arrow beside \cmd{main}.
For Python, right-click the file and choose \textbf{Run}. The default interpreter is PyPy.
If the Evaluation Feedback window appears, choose \textbf{No Thanks}.
\subsection*{IntelliJ IDEA}
Open the folder containing your Java file.
Use the green arrow beside \cmd{main}, then choose \textbf{Run}.
New projects use Java 25 and the runtime options given in the Java section.
\subsection*{PyCharm}
Open the folder containing your Python file.
Right-click the file and choose \textbf{Run}, or use the green arrow.
The default interpreter is PyPy.
\subsection*{Eclipse}
You can use Eclipse for C, C++, Java and Python~3.
The Applications menu opens the contest workspace.
Put your files in \cmd{\textasciitilde/contest}, expand the \textbf{contest} project and open your source file.
From the external-tools dropdown, shown as a green arrow with a toolbox, choose \textbf{ACPC Build and run}.
Type input in the Console panel.
For Java, use this action to compile with \cmd{javac} and the Kattis options.
The ordinary Java Run action uses Eclipse's Java compiler instead.
Select the source file again before starting another run.
If you copy files into the folder, press F5 in the project tree to show them.
\subsection*{Project settings}
No compiler path needs to be entered for the actions above.
You can skip the theme tour if it appears when you first open CLion, IntelliJ IDEA or PyCharm.
An existing project, a different Eclipse workspace, a new Python virtual environment or a compiler you select yourself can change these settings.
Use the supplied workspace and default interpreter, or check your program with the \cmd{compile*} and \cmd{run*} commands before submitting.
\section*{Your machine}
\begin{itemize}
\item You are logged in automatically with the account \cmd{team}.
Keep your files in your home directory (\cmd{/home/team}).
\item You can use CLion, Code::Blocks, Eclipse, IntelliJ IDEA, PyCharm or VS Code.
Vim, Gvim, Emacs, Geany, Kate, gedit and nano are also available.
They are in the Applications menu, except nano, which runs in a terminal.
\item The documentation of the languages is at \url{https://devdocs/} (bookmark ``Language Documentation'').
It has the documentation of C, C++, Java 25 and Python 3.11, and works without Internet access.
In a terminal, \cmd{man 3 printf} shows the manual of a C library function and \cmd{pypy3 -m pydoc heapq} the documentation of a Python module.
\item ``Contest Help (offline)'' in the bookmarks bar and the Contest menu contains searchable summaries about input and output, judging, local testing and common C, C++, Java and Python questions.
It works without Internet access. The source links in the articles lead to the online Kattis help portal, which is unavailable during the contest.
\item The network only gives access to the contest site, the Kattis status page and the documentation.
Every other website is blocked.
\end{itemize}
\end{document}
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Contest firewall DNS filter (answers only the allowed names)
After=network.target firewall.service
PartOf=firewall.service
# Started and stopped with firewall.service (no [Install] section on purpose)
[Service]
Type=simple
ExecStart=/usr/sbin/dnsmasq --keep-in-foreground --conf-file=/run/contest-firewall/dnsmasq.conf
Restart=always
RestartSec=2
+50
View File
@@ -0,0 +1,50 @@
# Local proxy of the contest firewall (separate from the nginx of the image).
# The firewall redirects every outgoing connection on port 443 to this proxy,
# which reads the site name sent by the browser (TLS SNI), refuses names that
# are not in allowed_domains.txt and connects to the allowed sites itself.
# The proxy resolves the names on its own, so a program that forces the
# address of an allowed site for another name gets nowhere. Port 80 only
# answers with a redirect to HTTPS.
load_module /usr/lib/nginx/modules/ngx_stream_module.so;
user contest-proxy;
pid /run/contest-proxy.pid;
worker_processes 2;
worker_rlimit_nofile 8192;
error_log stderr warn;
events {
worker_connections 2048;
}
stream {
# resolver line (the local DNS filter), written by /firewall.sh
include /run/contest-firewall/proxy_resolver.conf;
map $ssl_preread_server_name $upstream {
hostnames;
# one "<name> <name>:443;" line per allowed domain, written by /firewall.sh
include /run/contest-firewall/proxy_names.conf;
default "";
}
log_format sni '$remote_addr "$ssl_preread_server_name" $status $upstream_addr';
access_log syslog:server=unix:/dev/log,tag=contest_proxy,nohostname sni;
server {
listen 127.0.0.1:8443;
ssl_preread on;
preread_timeout 10s;
proxy_connect_timeout 10s;
proxy_pass $upstream;
}
}
http {
access_log off;
server {
listen 127.0.0.1:8080;
return 301 https://$host$request_uri;
}
}
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Contest firewall proxy (checks the site name of HTTPS connections)
After=network.target firewall.service
PartOf=firewall.service
# Started and stopped with firewall.service (no [Install] section on purpose)
[Service]
Type=simple
ExecStartPre=/usr/sbin/nginx -t -q -c /etc/contest-firewall/contest-proxy.conf
ExecStart=/usr/sbin/nginx -c /etc/contest-firewall/contest-proxy.conf -g 'daemon off;'
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=2
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/python3
"""Install contest compiler defaults for the installed IDEs and clean team homes."""
import configparser
import io
import json
import os
from pathlib import Path
import pwd
import subprocess
import xml.etree.ElementTree as ET
changed = 0
JAVA_HOME = '/usr/lib/jvm/java-25-openjdk-amd64'
JAVA_OPTIONS = '-Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2 -Xms2048m -Xmx2048m'
def write(path, text):
global changed
path = Path(path)
if not path.exists() or path.read_text() != text:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
changed += 1
def write_json(path, data):
write(path, json.dumps(data, indent=2) + '\n')
def read_json(path, default):
return json.loads(path.read_text()) if path.exists() else default
def geany(language, commands):
path = Path('/usr/share/geany/filedefs/filetypes.' + language)
conf = configparser.ConfigParser(interpolation=None, strict=False)
conf.optionxform = str
conf.read(path)
if not conf.has_section('build-menu'):
conf.add_section('build-menu')
for key, value in commands.items():
conf.set('build-menu', key, value)
stream = io.StringIO()
conf.write(stream, space_around_delimiters=False)
write(path, stream.getvalue())
for lang, cc in [('c', 'acpc-gcc'), ('cpp', 'acpc-g++')]:
geany(lang, {'FT_00_LB': '_Compile', 'FT_00_CM': cc + ' -c "%f" -o "%e.o"',
'FT_00_WD': '', 'FT_01_LB': '_Build', 'FT_01_CM': cc + ' "%f" -o "%e"',
'FT_01_WD': '', 'EX_00_LB': '_Execute', 'EX_00_CM': '"./%e"', 'EX_00_WD': ''})
geany('java', {'FT_00_LB': '_Compile', 'FT_00_CM': 'compilejava "%f"', 'FT_00_WD': '',
'FT_01_LB': '_Build', 'FT_01_CM': 'compilejava "%f"', 'FT_01_WD': '',
'EX_00_LB': '_Execute', 'EX_00_CM': 'runjava "%e"', 'EX_00_WD': ''})
geany('python', {'FT_00_LB': '_Compile', 'FT_00_CM': 'pypy3 -m py_compile "%f"', 'FT_00_WD': '',
'FT_01_LB': '_Build', 'FT_01_CM': 'pypy3 -m py_compile "%f"', 'FT_01_WD': '',
'EX_00_LB': '_Execute', 'EX_00_CM': 'pypy3 "%f"', 'EX_00_WD': ''})
def child(parent, tag):
node = parent.find(tag)
return node if node is not None else ET.SubElement(parent, tag)
def string(parent, tag, value):
child(child(parent, tag), 'str').text = value
def component(parent, name):
for node in parent.findall('component'):
if node.get('name') == name:
return node
return ET.SubElement(parent, 'component', name=name)
def option(parent, name, value):
node = next((n for n in parent.findall('option') if n.get('name') == name), None)
if node is None:
node = ET.SubElement(parent, 'option', name=name)
node.set('value', value)
def write_xml(path, root):
ET.indent(root)
write(path, ET.tostring(root, encoding='unicode') + '\n')
def preferences(path, values):
"""Update Java .properties keys, preserving unrelated workspace choices."""
data = {}
if path.exists():
for line in path.read_text().splitlines():
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
data[key] = value
for key, value in {'eclipse.preferences.version': '1', **values}.items():
data[key] = str(value).replace('\\', '\\\\').replace('\n', r'\n').replace('=', r'\=')
write(path, ''.join(k + '=' + v + '\n' for k, v in sorted(data.items())))
# Ask PyDev's own probe for the library paths of the installed PyPy.
probe = next(Path('/opt/eclipse/plugins').glob('org.python.pydev.core_*/pysrc/interpreterInfo.py'))
pyinfo = ET.fromstring(subprocess.check_output(['/usr/local/bin/pypy3', str(probe)], text=True))
ET.SubElement(pyinfo, 'name').text = 'ACPC PyPy 3.11'
ET.SubElement(pyinfo, 'vmArgs').text = ''
for lib in pyinfo.findall('lib'):
lib.attrib.clear()
pyinfo_text = ET.tostring(pyinfo, encoding='unicode') + '&&&&&'
for home in (Path('/home/team'), Path('/etc/skel')):
# Code::Blocks uses these settings for both single files and new projects.
cb_path = home / '.config/codeblocks/default.conf'
cb = ET.fromstring(cb_path.read_text()) if cb_path.exists() else ET.Element('CodeBlocksConfig', version='1')
compiler = child(cb, 'compiler')
string(compiler, 'SETTINGS_VERSION', '0.0.3')
string(compiler, 'DEFAULT_COMPILER', 'gcc')
gcc = child(child(compiler, 'sets'), 'gcc')
# Code::Blocks prepends MASTER_PATH/bin even to an absolute compiler name.
for key, value in {'NAME': 'ACPC GCC 15', 'MASTER_PATH': '/usr/local',
'EXTRA_PATHS': '/usr/bin',
'C_COMPILER': 'acpc-gcc', 'CPP_COMPILER': 'acpc-g++',
'LINKER': 'acpc-g++', 'MAKE': 'make', 'LIB_LINKER': 'ar'}.items():
string(gcc, key, value)
ET.indent(cb)
write(cb_path, '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(cb, encoding='unicode') + '\n')
code = home / '.config/Code/User'
settings = read_json(code / 'settings.json', {})
settings.update({
'python.defaultInterpreterPath': '/usr/local/bin/pypy3',
'C_Cpp.default.compilerPath': '/usr/bin/g++-15',
'C_Cpp.default.cStandard': 'c23', 'C_Cpp.default.cppStandard': 'c++23',
'java.jdt.ls.java.home': JAVA_HOME,
'java.configuration.runtimes': [{'name': 'JavaSE-25', 'path': JAVA_HOME, 'default': True}],
'java.debug.settings.vmArgs': JAVA_OPTIONS,
'debug.onTaskErrors': 'abort',
})
write_json(code / 'settings.json', settings)
tasks = read_json(code / 'tasks.json', {'version': '2.0.0', 'tasks': []})
tasks['tasks'] = [t for t in tasks['tasks'] if not t.get('label', '').startswith('ACPC:')]
for action in ('build', 'run'):
task = {'label': 'ACPC: ' + ('Build' if action == 'build' else 'Build and run'),
'type': 'process', 'command': '/usr/local/bin/acpc-solution',
'args': [action, '${file}'], 'options': {'cwd': '${fileDirname}'},
'problemMatcher': ['$gcc'],
'presentation': {'reveal': 'always', 'focus': True, 'panel': 'shared', 'clear': True}}
if action == 'build': task['group'] = {'kind': 'build', 'isDefault': True}
tasks['tasks'].append(task)
write_json(code / 'tasks.json', tasks)
bindings = read_json(code / 'keybindings.json', [])
bindings = [b for b in bindings if not (b.get('key') == 'f6' or str(b.get('args', '')).startswith('ACPC:'))]
bindings.append({'key': 'f6', 'command': 'workbench.action.tasks.runTask',
'args': 'ACPC: Build and run', 'when': 'editorTextFocus'})
write_json(code / 'keybindings.json', bindings)
(home / 'contest').mkdir(exist_ok=True)
project_path = home / 'contest/.project'
if not project_path.exists():
write(project_path, '<projectDescription><name>contest</name><comment>ACPC solutions</comment><projects/><buildSpec/><natures/></projectDescription>\n')
preferences(home / 'contest/.settings/org.eclipse.core.resources.prefs', {'encoding/<project>': 'UTF-8'})
workspace = home / 'eclipse-workspace'
prefs = workspace / '.metadata/.plugins/org.eclipse.core.runtime/.settings'
preferences(prefs / 'org.python.pydev.prefs', {
'INTERPRETER_PATH_NEW': pyinfo_text,
# PyDev's first-editor prompt would disable automatic Java/CDT builds
# across this mixed-language workspace. Keep those defaults and skip it.
'CHECK_PREFERRED_PYDEV_SETTINGS': 'false',
})
preferences(prefs / 'org.eclipse.jdt.core.prefs', {
'org.eclipse.jdt.core.compiler.compliance': '25',
'org.eclipse.jdt.core.compiler.source': '25',
'org.eclipse.jdt.core.compiler.codegen.targetPlatform': '25',
})
vm_type = 'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType'
vm_id = 'acpc-java-25'
vms = ET.Element('vmSettings', defaultVM=f'{len(vm_type)},{vm_type}{len(vm_id)},{vm_id}', defaultVMConnector='')
vmtype = ET.SubElement(vms, 'vmType', id=vm_type)
ET.SubElement(vmtype, 'vm', id=vm_id, name='ACPC Java 25', path=JAVA_HOME, vmArgs=JAVA_OPTIONS)
preferences(prefs / 'org.eclipse.jdt.launching.prefs', {'org.eclipse.jdt.launching.PREF_VM_XML': ET.tostring(vms, encoding='unicode')})
preferences(prefs / 'org.eclipse.ui.prefs', {'showIntro': 'false'})
preferences(prefs / 'org.eclipse.core.resources.prefs', {'encoding': 'UTF-8'})
for action in ('build', 'run'):
title = 'ACPC ' + ('Build' if action == 'build' else 'Build and run')
launch = ET.Element('launchConfiguration', type='org.eclipse.ui.externaltools.ProgramLaunchConfigurationType')
for key, value in {
'org.eclipse.ui.externaltools.ATTR_LOCATION': '/usr/local/bin/acpc-solution',
'org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS': action + ' "${resource_loc}"',
'org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY': '${container_loc}',
'org.eclipse.debug.core.ATTR_REFRESH_SCOPE': '${workspace}',
}.items():
ET.SubElement(launch, 'stringAttribute', key=key, value=value)
for key, value in {
'org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND': 'true',
'org.eclipse.debug.core.ATTR_CAPTURE_OUTPUT': 'true',
'org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON': 'true',
'org.eclipse.debug.ui.ATTR_CONSOLE_ERROR_ON': 'true',
}.items():
ET.SubElement(launch, 'booleanAttribute', key=key, value=value)
favorites = ET.SubElement(launch, 'listAttribute', key='org.eclipse.debug.ui.favoriteGroups')
ET.SubElement(favorites, 'listEntry', value='org.eclipse.ui.externaltools.launchGroup')
write_xml(workspace / '.metadata/.plugins/org.eclipse.debug.core/.launches' / (title + '.launch'), launch)
history_path = workspace / '.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml'
history = ET.fromstring(history_path.read_text()) if history_path.exists() else ET.Element('launchHistory')
group_id = 'org.eclipse.ui.externaltools.launchGroup'
group = next((g for g in history if g.get('id') == group_id), None)
if group is None:
group = ET.SubElement(history, 'launchGroup', id=group_id)
child(group, 'mruHistory')
favorites = child(group, 'favorites')
for title in ('ACPC Build and run', 'ACPC Build'):
if not any(title == ET.fromstring(n.get('memento', '<x/>')).get('path') for n in favorites):
ET.SubElement(favorites, 'launch', memento=ET.tostring(ET.Element('launchConfiguration', local='true', path=title), encoding='unicode'))
write_xml(history_path, history)
for ide in ('clion', 'intellij-idea', 'pycharm'):
info = Path('/opt') / ide / 'product-info.json'
if not info.exists():
continue
options = home / '.config/JetBrains' / json.loads(info.read_text())['dataDirectoryName'] / 'options'
default_file = options / 'project.default.xml'
defaults = ET.fromstring(default_file.read_text()) if default_file.exists() else ET.Element('application')
project = child(component(defaults, 'ProjectManager'), 'defaultProject')
runs = component(project, 'RunManager')
if ide in ('pycharm', 'clion'):
sdk_file = options / 'jdk.table.xml'
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
table = component(sdks, 'ProjectJdkTable')
sdk_name = 'PyPy 3.11 (as on Kattis)'
for node in list(table):
if node.find('name') is not None and node.find('name').get('value') == sdk_name:
table.remove(node)
sdk = ET.SubElement(table, 'jdk', version='2')
for key, value in [('name', sdk_name), ('type', 'Python SDK'),
('version', 'Python 3.11.15'), ('homePath', '/usr/local/bin/pypy3')]:
ET.SubElement(sdk, key, value=value)
roots = ET.SubElement(sdk, 'roots')
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
for lib in pyinfo.findall('lib'):
ET.SubElement(cp, 'root', url='file://' + lib.text, type='simple')
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
additional = ET.SubElement(sdk, 'additional', SDK_UUID='3f7d6c1e-2a4b-4c8d-9e0f-a1b2c3d4e5f6')
ET.SubElement(additional, 'setting', name='FLAVOR_ID', value='PyPySdkFlavor')
ET.SubElement(additional, 'setting', name='FLAVOR_DATA', value='{}')
write_xml(sdk_file, sdks)
root_manager = component(project, 'ProjectRootManager')
root_manager.attrib.update({'version': '2', 'project-jdk-name': sdk_name, 'project-jdk-type': 'Python SDK'})
if ide == 'clion':
chain_file = options / 'linux/toolchains.xml'
chains = ET.fromstring(chain_file.read_text()) if chain_file.exists() else ET.Element('application')
cpp = component(chains, 'CPPToolchains')
cpp.set('version', '10')
entries = child(cpp, 'toolchains')
entries.set('detectedVersion', '5')
chain = entries.find('toolchain')
if chain is None:
chain = ET.SubElement(entries, 'toolchain', name='Default', toolSetKind='SYSTEM_UNIX_TOOLSET', debuggerKind='BUNDLED_GDB')
chain.set('customCCompilerPath', '/usr/local/bin/acpc-gcc')
chain.set('customCXXCompilerPath', '/usr/local/bin/acpc-g++')
write_xml(chain_file, chains)
for node in list(runs):
if node.get('default') == 'true' and node.get('type') == 'CppFileRunConfiguration':
runs.remove(node)
cfg = ET.SubElement(runs, 'configuration', default='true', type='CppFileRunConfiguration', factoryName='CppFileRunConfiguration')
option(cfg, 'compilerFile', '/usr/local/bin/acpc-cc')
option(cfg, 'compilerOptions', '')
if ide == 'intellij-idea':
sdk_file = options / 'jdk.table.xml'
sdks = ET.fromstring(sdk_file.read_text()) if sdk_file.exists() else ET.Element('application')
table = component(sdks, 'ProjectJdkTable')
for node in list(table):
if node.find('name') is not None and node.find('name').get('value') == 'ACPC Java 25':
table.remove(node)
sdk = ET.SubElement(table, 'jdk', version='2')
for key, value in [('name', 'ACPC Java 25'), ('type', 'JavaSDK'),
('version', 'java version "25.0.4.1"'), ('homePath', JAVA_HOME)]:
ET.SubElement(sdk, key, value=value)
roots = ET.SubElement(sdk, 'roots')
cp = ET.SubElement(ET.SubElement(roots, 'classPath'), 'root', type='composite')
for module in sorted(Path(JAVA_HOME, 'jmods').glob('*.jmod')):
ET.SubElement(cp, 'root', url=f'jrt://{JAVA_HOME}!/{module.stem}', type='simple')
ET.SubElement(ET.SubElement(roots, 'sourcePath'), 'root', type='composite')
ET.SubElement(sdk, 'additional')
write_xml(sdk_file, sdks)
root_manager = component(project, 'ProjectRootManager')
root_manager.attrib.update({'version': '2', 'languageLevel': 'JDK_25',
'project-jdk-name': 'ACPC Java 25', 'project-jdk-type': 'JavaSDK'})
for kind, factory in [('Application', 'Application'), ('JavaScratch', 'Java Scratch')]:
for node in list(runs):
if node.get('default') == 'true' and node.get('type') == kind:
runs.remove(node)
cfg = ET.SubElement(runs, 'configuration', default='true', type=kind, factoryName=factory)
option(cfg, 'VM_PARAMETERS', JAVA_OPTIONS)
option(cfg, 'ALTERNATIVE_JRE_PATH', JAVA_HOME)
option(cfg, 'ALTERNATIVE_JRE_PATH_ENABLED', 'true')
encoding = component(project, 'Encoding')
for node in list(encoding):
if node.get('url') == 'PROJECT':
encoding.remove(node)
ET.SubElement(encoding, 'file', url='PROJECT', charset='UTF-8')
write_xml(default_file, defaults)
if home == Path('/home/team'):
account = pwd.getpwnam('team')
for folder in (home / '.config/codeblocks', code, home / 'contest', home / '.config/JetBrains', workspace):
for path in [folder, *folder.rglob('*')]:
os.chown(path, account.pw_uid, account.pw_gid)
print(f'changed={changed}')
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Usage: jetbrains_first_run.sh HOME_DIR
# Settings of the JetBrains IDEs (CLion, IntelliJ IDEA, PyCharm) for the team
# account, so that the teams go straight to work:
# - user agreement accepted, no usage data sharing (first-launch questions)
# - the home directory /home/team trusted (no "Trust project?" question)
# Compiler and interpreter defaults are installed by configure_ides.py.
# The folder name under .java is how Java stores the preference node
# "privacy_policy".
set -eu
dest_home=$1
prefs="$dest_home/.java/.userPrefs/jetbrains/"$'_!(!!cg"p!(}!}@"j!(k!|w"w!\'8!b!"p!\':!e@=='
consent="$dest_home/.local/share/JetBrains/consentOptions"
mkdir -p "$prefs" "$consent"
cat > "$prefs/prefs.xml" <<'XML'
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE map SYSTEM "http://java.sun.com/dtd/preferences.dtd">
<map MAP_XML_VERSION="1.0">
<entry key="eua_accepted_version" value="2.0"/>
</map>
XML
printf 'rsch.send.usage.stat:1.1:0:1791991599000' > "$consent/accepted"
for ide in pycharm intellij-idea clion; do
[ -f "/opt/$ide/product-info.json" ] || continue
dir=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["dataDirectoryName"])' "/opt/$ide/product-info.json")
options="$dest_home/.config/JetBrains/$dir/options"
mkdir -p "$options"
cat > "$options/trusted-paths.xml" <<'XML'
<application>
<component name="Trusted.Paths.Settings">
<option name="TRUSTED_PATHS">
<list>
<option value="/home/team" />
</list>
</option>
</component>
</application>
XML
done
if [ "$dest_home" != /etc/skel ]; then
owner=$(stat -c %U:%G "$dest_home")
chown -R "$owner" "$dest_home/.java" "$dest_home/.local" "$dest_home/.config/JetBrains"
fi
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Turns the contest firewall off (all traffic allowed) for maintenance.
systemctl stop firewall.service
systemctl disable firewall.service
systemctl stop refresh_rules.service
systemctl stop refresh_rules.timer
systemctl disable refresh_rules.service
systemctl disable refresh_rules.timer
nft flush ruleset
nft delete table inet filter 2>/dev/null
echo "Firewall disabled. All traffic is now allowed."
# Reset DNS to its default behaviour (systemd-resolved instead of the filter)
ln -sfn ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl restart systemd-resolved
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Turns the contest firewall on and keeps it on across reboots.
systemctl enable firewall.service
systemctl start firewall.service
systemctl enable refresh_rules.service
systemctl start refresh_rules.service
systemctl enable refresh_rules.timer
systemctl start refresh_rules.timer
echo "Firewall enabled."
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=Contest firewall
Wants=network-online.target contest-proxy.service contest-dns.service
After=network.target network-online.target
Before=getty.target getty@tty1.service
[Service]
Type=oneshot
ExecStart=/firewall.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
# Base nftables ruleset: inbound traffic is dropped, outbound traffic is rejected
# except loopback, established connections, the local proxy (web), the local
# DNS filter and the time servers that /refresh_rules.sh adds.
# Web traffic (ports 80 and 443) of every program is redirected to the local
# proxy (contest-proxy.service), which checks the site name. Only the proxy
# may connect to the outside on port 443.
CONF=/etc/contest-firewall
# Remove any persisting nftables rules
rm -f /etc/nftables.rules
touch /etc/nftables.rules
# Configuration of the proxy: the allowed names and the DNS resolver
mkdir -p /run/contest-firewall
for name in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt"); do
echo "$name $name:443;"
done > /run/contest-firewall/proxy_names.conf
# The proxy resolves the names through the DNS filter below
echo "resolver 127.0.0.1 ipv6=off valid=30s;" > /run/contest-firewall/proxy_resolver.conf
# Local DNS filter (contest-dns.service): the allowed domains and the time
# servers are forwarded to the DNS resolvers, every other name gets no answer.
# Programs on the machine ask it through /etc/resolv.conf, and only the proxy
# account (which also runs the filter) may reach the DNS resolvers. This
# closes DNS as a way to exchange data with a server outside.
{
echo "listen-address=127.0.0.1"
echo "bind-interfaces"
echo "no-resolv"
echo "no-hosts"
echo "user=contest-proxy"
echo "group=nogroup"
echo "cache-size=1000"
echo "log-queries"
echo "server=/#/"
for name in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt" "$CONF/ntp_servers.txt"); do
for dns in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/dns_servers.txt"); do
echo "server=/$name/$dns"
done
done
} > /run/contest-firewall/dnsmasq.conf
# /run/resolvconf is one of the places the AppArmor profiles (dig, ...) accept,
# and the resolvconf package that would also use it is not installed
mkdir -p /run/resolvconf
echo "nameserver 127.0.0.1" > /run/resolvconf/resolv.conf
ln -sfn /run/resolvconf/resolv.conf /etc/resolv.conf
# Flush all rules, then create the table, chains and address sets
nft flush ruleset
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0 ; policy drop ; }'
nft add chain inet filter forward '{ type filter hook forward priority 0 ; policy drop ; }'
nft add chain inet filter output '{ type filter hook output priority 0 ; policy accept ; }'
nft add set inet filter ntp_ips '{ type ipv4_addr ; flags interval ; }'
# Redirect web traffic to the proxy, except the proxy's own connections and
# the local sites of the image (DevDocs)
nft add table inet nat
nft add chain inet nat output '{ type nat hook output priority -100 ; }'
nft add rule inet nat output meta skuid contest-proxy return
nft add rule inet nat output ip daddr 127.0.0.0/8 return
nft add rule inet nat output meta nfproto ipv4 tcp dport 443 redirect to :8443
nft add rule inet nat output meta nfproto ipv4 tcp dport 80 redirect to :8080
# Established/related connections and loopback
nft add rule inet filter input ct state related,established accept
nft add rule inet filter output ct state related,established accept
nft add rule inet filter input iif lo accept
nft add rule inet filter output oif lo accept
# Connections redirected to the proxy (they still show the network interface here)
nft add rule inet filter output ip daddr 127.0.0.1 tcp dport '{ 8080, 8443 }' ct status dnat accept
# Web: only the proxy connects to the outside, by name, to the allowed sites
# it resolves itself through the DNS filter (no list of addresses to keep up
# to date, content delivery networks change them all the time)
nft add rule inet filter output meta skuid contest-proxy tcp dport 443 accept
# Time servers, for chrony only (addresses filled by /refresh_rules.sh)
nft add rule inet filter output meta skuid _chrony ip daddr @ntp_ips udp dport 123 accept
# DNS resolvers, for the proxy and the DNS filter only
for dns in $(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/dns_servers.txt"); do
nft add rule inet filter output meta skuid contest-proxy ip daddr "$dns" udp dport 53 accept
nft add rule inet filter output meta skuid contest-proxy ip daddr "$dns" tcp dport 53 accept
done
# Reject any other outgoing connection
nft add rule inet filter output reject
+104
View File
@@ -0,0 +1,104 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
<title>Contest Help (offline) · ACPC 2026</title>
<style>
:root{color-scheme:light;--ink:#1c3043;--muted:#4a6275;--line:#d4e0e6;--accent:#14615c}
*{box-sizing:border-box}body{margin:0;background:#f3f6f8;color:var(--ink);font:17px/1.6 system-ui,sans-serif}
header{background:#153746;color:#fff;padding:36px max(24px,calc((100vw - 1180px)/2))}
header p{max-width:800px;color:#d4e8ed;margin:8px 0}h1{font-size:clamp(28px,4vw,42px);line-height:1.2;margin:8px 0 16px}
.eyebrow{font-size:14px;letter-spacing:.08em;text-transform:uppercase}a{color:#115f70;text-underline-offset:3px}a:focus-visible,button:focus-visible,input:focus-visible{outline:3px solid #c08619;outline-offset:3px}
.layout{max-width:1228px;margin:0 auto;display:grid;grid-template-columns:250px minmax(0,1fr);gap:32px;padding:32px 24px}
aside{align-self:start;position:sticky;top:24px}label{font-weight:650;display:block;margin-bottom:6px}input{font:inherit;padding:10px 12px;border:1px solid #8096a4;border-radius:6px;width:100%}button{font:inherit;padding:5px 12px;cursor:pointer;background:#fff;border:1px solid #8096a4;border-radius:5px;margin-top:8px}
nav{margin-top:24px}nav a{display:block;padding:5px 0;text-decoration:none}nav a:hover{text-decoration:underline}.count{color:var(--muted);font-size:14px;margin:8px 0}
article{background:#fff;padding:24px 28px;border:1px solid var(--line);border-radius:8px;margin:0 0 20px;scroll-margin-top:20px}article[hidden]{display:none}h2{font-size:24px;line-height:1.3;margin:0 0 14px}h3{font-size:18px;margin:18px 0 6px}p{margin:10px 0}li{margin:6px 0}ul,ol{padding-left:22px}code,pre{font-family:ui-monospace,monospace;font-size:.91em}code{background:#eef3f5;padding:1px 4px;border-radius:3px}pre{overflow-x:auto;background:#eef3f5;padding:16px;border-radius:5px;line-height:1.5}pre code{padding:0}
table{border-collapse:collapse;width:100%;font-size:16px}th,td{text-align:left;vertical-align:top;padding:10px 8px;border-bottom:1px solid var(--line)}th:first-child,td:first-child{width:160px}.sources{font-size:14px;color:var(--muted);border-top:1px solid var(--line);padding-top:12px;margin-top:20px}.sources a{margin-right:10px}footer{max-width:1180px;margin:0 auto;padding:0 24px 32px;color:var(--muted);font-size:14px}.empty{padding:30px;background:#fff;border:1px solid var(--line)}
@media(max-width:760px){.layout{display:block;padding:20px 16px}aside{position:static;margin-bottom:24px}nav{display:flex;flex-wrap:wrap;gap:4px 18px}article{padding:20px}header{padding:24px}th:first-child,td:first-child{width:120px}}
@media print{body{background:#fff;font-size:11pt}header{background:#fff;color:#000;padding:0}header p{color:#000}.layout{display:block;padding:0}aside,button{display:none}article,article[hidden]{display:block;border:0;padding:0;break-inside:avoid}a{color:inherit}.sources{font-size:9pt}footer{padding:0}}
</style>
</head>
<body>
<header><div class="eyebrow">ACPC 2026 · Local reference</div><h1>Contest Help (offline)</h1><p>Input, judging and language questions for C, C++, Java and Python 3. All articles on this page work without Internet access.</p></header>
<div class="layout">
<aside aria-label="Find an article">
<label for="search">Search help</label><input id="search" type="search" placeholder="Try EOF, Java or memory" autocomplete="off"><button id="clear" type="button">Clear search</button><p id="count" class="count" role="status" aria-live="polite">12 articles</p>
<nav aria-label="Articles">
<a href="#input-output">Input and output</a><a href="#eof">Reading until EOF</a><a href="#valid-input">Valid input</a><a href="#local-tests">Testing locally</a><a href="#judging">How judging works</a><a href="#judgements">Judgements</a><a href="#rejected">Investigating a rejection</a><a href="#c-cpp">C and C++</a><a href="#java-main">Java main class</a><a href="#java-io">Java input and formatting</a><a href="#java-errors">Java exceptions</a><a href="#python">Python 3 and modules</a>
</nav>
<p class="count"><a href="../../icpc/TechNotes.pdf">Technical Notes</a><br><a href="https://devdocs/">Language Documentation</a></p>
</aside>
<main id="articles">
<p id="empty" class="empty" hidden>No matching article. Try a shorter search or clear the search.</p>
<article id="input-output"><h2>Input and output</h2>
<p>Read the problem's input from standard input and write the answer to standard output. Follow the format in the statement. Do not print prompts, progress messages or debugging text as part of the answer.</p>
<p>For local tests, save the input in a file and redirect it into your program. Redirection supplies the file contents through standard input, just as typing would. You do not need to open a named input file in the program.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000120852-how-do-i-handle-input-and-output-">Kattis input and output</a></p></article>
<article id="eof"><h2>Reading until EOF</h2>
<p>Some problems provide a test count or a terminating value. Others end when no input remains. Follow the rule in the statement.</p>
<p>End of file, or EOF, is a condition reported by a read operation. It is not a word to enter in the input. A redirected input file ends automatically. In a terminal, finish your input line with Enter, then press Ctrl+D on an empty line to signal EOF.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000120853-how-do-i-read-input-if-the-end-is-not-defined-what-is-end-of-file-">Kattis end of file</a></p></article>
<article id="valid-input"><h2>Valid input</h2>
<p>The judge's input follows the problem's input specification. Your solution can rely on the stated ranges and structure. Include the boundary values that the statement allows when constructing local tests.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000121166-how-do-i-handle-input-that-does-not-follow-the-input-specification-">Kattis input specification</a></p></article>
<article id="local-tests"><h2>Testing locally</h2>
<p>Use the sample input and expected output from the problem page, then test additional cases. Passing the samples does not prove the solution is correct for every permitted input.</p>
<p>For a C++ file named <code>main.cpp</code> and a sample saved as <code>sample.in</code>, the contest machine provides these commands:</p>
<pre><code>compilecpp main.cpp
runcpp &lt; sample.in &gt; actual.out
diff -u sample.ans actual.out</code></pre>
<p>Save the expected output in <code>sample.ans</code>. Read the <a href="../../icpc/TechNotes.pdf">Technical Notes</a> for the equivalent commands in the other languages and for IDE build actions. Local runs do not enforce the judge's time and memory limits. A text comparison can help locate differences, but the problem's output rules determine whether an answer is valid.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122495-can-i-test-my-solution-before-i-submit-">Kattis local testing</a>. Commands are specific to the ACPC image.</p></article>
<article id="judging"><h2>How judging works</h2>
<p>Kattis first compiles the submission or checks it as required by the chosen language. It then runs the program on test inputs and checks the answers against the problem's rules. Each test run starts a new process, so program variables do not carry over between test files.</p>
<p>The judge enforces resource limits and may stop checking after a failure. A rejection therefore does not describe every possible problem with the submission. Some problems allow several valid answers or a specified numerical tolerance. Read their output requirements carefully.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000141262-how-judging-works">Kattis judging process</a></p></article>
<article id="judgements"><h2>Judgements</h2>
<table><thead><tr><th scope="col">Result</th><th scope="col">Meaning</th></tr></thead><tbody>
<tr><th scope="row">Accepted</th><td>The submission passed the required checks.</td></tr>
<tr><th scope="row">Compile Error</th><td>The build or language check failed. Read the compiler output.</td></tr>
<tr><th scope="row">Run Time Error</th><td>The program crashed, raised an uncaught exception or ended with a nonzero exit code.</td></tr>
<tr><th scope="row">Time Limit Exceeded</th><td>A run exceeded the time allowed for a test.</td></tr>
<tr><th scope="row">Wrong Answer</th><td>An answer did not satisfy the output requirements.</td></tr>
<tr><th scope="row">Output Limit Exceeded</th><td>The program produced too much output, possibly because of a loop or debugging prints.</td></tr>
<tr><th scope="row">Memory Limit Exceeded</th><td>The run needed more memory than allowed. Memory exhaustion can also appear as a run time error.</td></tr>
<tr><th scope="row">Judge Error</th><td>The judging system encountered a problem. Ask contest staff.</td></tr>
</tbody></table>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000141261-list-of-possible-judgements">Kattis judgement list</a></p></article>
<article id="rejected"><h2>Investigating a rejection</h2>
<p>Read the judgement and any compiler or run feedback available on the submission page. Check the selected language and submitted files, then reproduce the issue locally where possible.</p>
<p>Recheck input parsing, output formatting and the limits in the statement. Test small cases that you can solve by hand, boundary values and cases with different input structures. Remove debugging output before submitting again. Hidden test inputs are generally unavailable, so the samples alone are not a complete test suite.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000030851-my-submissions-are-getting-rejected-what-can-i-do-">Rejected submissions</a><a href="https://support.kattis.com/support/solutions/articles/79000138570-why-don-t-you-provide-the-secret-test-data-">Hidden test data</a></p></article>
<article id="c-cpp"><h2>C and C++</h2>
<p>Use an <code>int main(...)</code> entry point and finish successful execution with a zero exit status. A nonzero exit status is a run time error even if the printed answer looks correct.</p>
<p>For C++ programs with substantial input or output, stream synchronization and repeated flushing can be expensive. Before any stream I/O, <code>std::ios::sync_with_stdio(false);</code> and <code>std::cin.tie(nullptr);</code> can reduce that overhead. After disabling synchronization, keep input and output within the C++ stream API. Prefer <code>'\n'</code> to <code>std::endl</code> when a flush is unnecessary.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122476-what-happens-if-main-does-not-return-0-">C exit status</a><a href="https://support.kattis.com/support/solutions/articles/79000122478-what-happens-if-main-does-not-return-0-">C++ exit status</a><a href="https://support.kattis.com/support/solutions/articles/79000122479-i-m-having-trouble-with-i-o-performance-with-cin-and-cout-in-c-what-can-i-do-about-it-">C++ I/O performance</a></p></article>
<article id="java-main"><h2>Java main class</h2>
<p>The main class is the class containing <code>public static void main(String[] args)</code>. Enter its class name, without <code>.java</code> or <code>.class</code>, when Kattis asks for it. If the class belongs to a package, the main class name includes that package.</p>
<p>For the local single-file workflow in the Technical Notes, use a file such as <code>Main.java</code>, a class named <code>Main</code> and no package declaration. Compile with <code>compilejava Main.java</code> and run with <code>runjava Main</code>.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122481-what-is-the-mainclass-for-java-why-do-i-get-unknown-mainclass-">Kattis Java main class</a>. Local commands are specific to the ACPC image.</p></article>
<article id="java-io"><h2>Java input and formatting</h2>
<p><code>Scanner</code> is convenient but may be too slow for large inputs. Buffered reading and writing can reduce I/O overhead. Read the input specification before choosing how to split lines or tokens.</p>
<p>Locale affects formatted numbers. Kattis documents the <code>en_US</code> locale for Java, which uses a decimal point. For predictable local formatting, specify <code>java.util.Locale.US</code> where the formatting API accepts a locale, and follow the problem's requested precision.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122482-i-m-having-trouble-with-i-o-performance-what-can-i-do-about-it-">Java I/O performance</a><a href="https://support.kattis.com/support/solutions/articles/79000122483-what-locale-is-used-useful-for-e-g-printf-in-java-">Java locale</a></p></article>
<article id="java-errors"><h2>Java exceptions</h2>
<p>An uncaught exception normally causes a run time error. Memory exhaustion can be reported as a memory limit error. The feedback available from Kattis may be shorter than the stack trace you see locally.</p>
<p>When reproducing a failure, inspect the local exception and the line where it arose. Check bounds, parsing, null references and memory use against the problem's valid inputs.</p>
<p class="sources">Source (online): <a href="https://support.kattis.com/support/solutions/articles/79000122480-what-happens-with-java-exceptions-">Kattis Java exceptions</a></p></article>
<article id="python"><h2>Python 3 and modules</h2>
<p>The ACPC Python 3 runtime is PyPy 7.3.23, implementing Python 3.11.15. Use the configured PyPy interpreter or the <code>compilepython3</code> and <code>runpython3</code> commands for local tests. CPython and PyPy can differ in performance and some implementation details.</p>
<p>Use the Python standard library available in the contest runtime. Do not assume that a third-party module installed elsewhere will be available on the judge. An import of an unavailable module fails at run time. The local Python 3.11 documentation is available in <a href="https://devdocs/">Language Documentation</a>.</p>
<p class="sources">Sources (online): <a href="https://support.kattis.com/support/solutions/articles/79000122490-what-implementation-of-python-are-you-using-">Python implementation</a><a href="https://support.kattis.com/support/solutions/articles/79000122491-what-python-modules-are-available-">Python modules</a>. Version numbers are from the ACPC configuration.</p></article>
</main></div>
<footer>Prepared for ACPC 2026 from the linked Kattis support articles, checked on 15 October 2026. These are concise summaries with local instructions. The contest rules and problem statements take precedence. Source links require Internet access outside the contest restrictions.</footer>
<script>
const search=document.getElementById('search');
const articles=[...document.querySelectorAll('article')];
function filter(){const words=search.value.toLowerCase().trim().split(/\s+/).filter(Boolean);let count=0;for(const a of articles){a.hidden=!words.every(w=>a.textContent.toLowerCase().includes(w));if(!a.hidden)count++}document.getElementById('count').textContent=count+' of '+articles.length+' articles';document.getElementById('empty').hidden=count!==0}
search.addEventListener('input',filter);
document.getElementById('clear').addEventListener('click',()=>{search.value='';filter();search.focus()});
document.querySelectorAll('nav a').forEach(a=>a.addEventListener('click',()=>{search.value='';filter()}));
</script>
</body></html>
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/python3
"""GCC 15 driver used by IDE toolchains, with the contest language and link options."""
import json
import os
from pathlib import Path
import re
import shlex
import sys
def expand_response_files(arguments, depth=0):
# CLion also puts its language/standard probes in GCC response files.
# Inspect their contents before choosing C vs C++ or replacing flags.
if depth > 16:
raise SystemExit('ACPC: response files are nested too deeply')
expanded = []
for arg in arguments:
if arg.startswith('@') and Path(arg[1:]).is_file():
expanded.extend(expand_response_files(shlex.split(Path(arg[1:]).read_text()), depth + 1))
else:
expanded.append(arg)
return expanded
args = expand_response_files(sys.argv[1:])
name = Path(sys.argv[0]).name
sources = [a for a in args if Path(a).suffix in ('.c', '.C', '.cc', '.cpp', '.cxx', '.c++')]
explicit_cxx = any(a.startswith('-xc++') or (a == '-x' and i + 1 < len(args) and args[i + 1].startswith('c++'))
for i, a in enumerate(args))
cxx = name.endswith(('g++', 'c++')) or explicit_cxx or (name not in ('acpc-gcc', 'gcc', 'cc') and any(not a.endswith('.c') for a in sources))
compiler = '/usr/bin/g++-15' if cxx else '/usr/bin/gcc-15'
query = any(a in ('--version', '-dumpversion', '-dumpfullversion', '-dumpmachine', '-print-search-dirs')
or a.startswith(('-print-file-name=', '-print-prog-name=')) for a in args)
if not query:
args = [a for a in args if not a.startswith('-std=') and not re.fullmatch(r'-O(?:[0-3sgz]|fast)?', a)]
args = ['-g', '-O2', '-std=gnu++23' if cxx else '-std=gnu23'] + args
if not any(a in args for a in ('-c', '-S', '-E', '-M', '-MM', '-fsyntax-only', '-shared')):
args += ['-static']
args += ['-lrt', '-Wl,--whole-archive', '-lpthread', '-Wl,--no-whole-archive'] if cxx else ['-lm']
cmd = [compiler] + args
if not query and sources:
print('ACPC: ' + shlex.join(cmd), file=sys.stderr, flush=True)
if log := os.environ.get('ACPC_COMPILER_LOG'):
with open(log, 'a') as f:
f.write(json.dumps({'cwd': os.getcwd(), 'argv': cmd}) + '\n')
os.execv(compiler, cmd)
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Initialize the supplied contest workspace once, also after a team-home reset.
set -eu
export PATH="/usr/local/lib/acpc-toolchain:$PATH"
workspace="$HOME/eclipse-workspace"
mkdir -p "$workspace"
if [ ! -f "$workspace/.acpc-imported" ]; then
/opt/eclipse/eclipse -nosplash -data "$workspace" \
-application org.eclipse.cdt.managedbuilder.core.headlessbuild \
-import "$HOME/contest" -no-indexer > "$workspace/import.log" 2>&1
touch "$workspace/.acpc-imported"
fi
exec /opt/eclipse/eclipse -data "$workspace" "$@"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/python3
"""Build or build-and-run the active contest source from an IDE task."""
import os
from pathlib import Path
import resource
import shlex
import subprocess
import sys
if len(sys.argv) != 3 or sys.argv[1] not in ('build', 'run'):
raise SystemExit('Usage: acpc-solution build|run SOURCE')
action, arg = sys.argv[1:]
src = Path(arg).resolve(strict=True)
os.chdir(src.parent)
ext = src.suffix
if ext == '.c':
build = ['/usr/local/bin/acpc-gcc', src.name, '-o', src.stem]
run = [str(src.with_suffix(''))]
elif ext in ('.cpp', '.cc', '.C', '.cxx', '.c++'):
build = ['/usr/local/bin/acpc-g++', src.name, '-o', src.stem]
run = [str(src.with_suffix(''))]
elif ext == '.java':
build = ['/usr/lib/jvm/java-25-openjdk-amd64/bin/javac', '-encoding', 'UTF-8',
'-sourcepath', '.', '-cp', './*', '-d', '.', src.name]
run = ['/usr/local/bin/runjava', src.stem]
elif ext in ('.py', '.py3'):
build = ['/usr/local/bin/pypy3', '-m', 'py_compile', src.name]
run = ['/usr/local/bin/pypy3', src.name]
else:
raise SystemExit('Use a C, C++, Java or Python source file.')
print('Build: ' + shlex.join(build), flush=True)
result = subprocess.run(build)
if result.returncode:
raise SystemExit(result.returncode)
if action == 'run':
print('Run: ' + shlex.join(run), flush=True)
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
resource.setrlimit(resource.RLIMIT_STACK, (hard, hard))
os.execv(run[0], run)
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a C program as Kattis does for ACPC 2026 (gcc-15 15.2.0). Result: a.out
exec gcc-15 -g -O2 -o a.out -std=gnu23 -static "$@" -lm
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a C++ program as Kattis does for ACPC 2026 (g++-15 15.2.0). Result: a.out
exec g++-15 -g -O2 -o a.out -std=gnu++23 -static -lrt -Wl,--whole-archive -lpthread -Wl,--no-whole-archive "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Compiles a Java program as Kattis does for ACPC 2026 (javac 25.0.4.1)
exec javac -encoding UTF-8 -sourcepath . -cp './*' -d . "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Checks the syntax of a Python 3 program with PyPy (Kattis runs it with pypy3 7.3.23)
exec pypy3 -m py_compile "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Runs the program compiled by compilec (a.out)
# Large stack (2 GB), for deep recursion
ulimit -s 2097152
exec ./a.out "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Runs the program compiled by compilecpp (a.out)
# Large stack (2 GB), for deep recursion
ulimit -s 2097152
exec ./a.out "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Runs a Java program as Kattis does for ACPC 2026 (java 25.0.4.1).
# Usage: runjava MainClass. Kattis sets the memory to the limit of the problem,
# 2048 MB here by default (MEMLIM=1024 runjava MainClass for another limit).
MEMLIM=${MEMLIM:-2048}
exec java -Dfile.encoding=UTF-8 -XX:+UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2 -Xms${MEMLIM}m -Xmx${MEMLIM}m -cp '.:./*' "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Runs a Python 3 program as Kattis does for ACPC 2026 (pypy3 7.3.23, Python 3.11.15)
exec pypy3 "$@"
@@ -0,0 +1,9 @@
// ACPC 2026: the teams may not change the network (Wi-Fi, phone hotspot,
// modem, VPN, ...). Only the administrators (sudo group) keep these actions.
polkit.addRule(function(action, subject) {
if ((action.id.indexOf("org.freedesktop.NetworkManager.") == 0 ||
action.id.indexOf("org.freedesktop.ModemManager1.") == 0) &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,14 @@
// ACPC 2026: the teams may not restart, power off, suspend or hibernate the
// machine. A restart would let USBGuard accept a device plugged in before.
// Only the administrators (sudo group) keep these actions.
polkit.addRule(function(action, subject) {
if ((action.id.indexOf("org.freedesktop.login1.reboot") == 0 ||
action.id.indexOf("org.freedesktop.login1.power-off") == 0 ||
action.id.indexOf("org.freedesktop.login1.halt") == 0 ||
action.id.indexOf("org.freedesktop.login1.suspend") == 0 ||
action.id.indexOf("org.freedesktop.login1.hibernate") == 0 ||
action.id.indexOf("org.freedesktop.login1.set-reboot") == 0) &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,8 @@
// ACPC 2026: the teams may not mount USB sticks, disks or disk images.
// Only the administrators (sudo group) keep the udisks actions.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.udisks2.") == 0 &&
!subject.isInGroup("sudo")) {
return polkit.Result.NO;
}
});
@@ -0,0 +1,7 @@
# ACPC 2026: the desktop user gets no direct access to phones and cameras
# (MTP and PTP, used to copy files), which bypasses the mounting of disks.
# The tags are set by 69-libmtp.rules and 60-libgphoto2, and used by
# 73-seat-late.rules, so this file sits between them.
ENV{ID_MTP_DEVICE}=="1", TAG-="uaccess"
ENV{ID_GPHOTO2}=="1", TAG-="uaccess"
ENV{ID_MEDIA_PLAYER}=="1", TAG-="uaccess"
+8
View File
@@ -0,0 +1,8 @@
# ACPC 2026: the power, reboot and suspend keys do nothing. A long press on
# the power button still cuts the power (hardware), in view of the proctors.
[Login]
HandlePowerKey=ignore
HandleRebootKey=ignore
HandleSuspendKey=ignore
HandleHibernateKey=ignore
HandleLidSwitch=ignore
+2
View File
@@ -0,0 +1,2 @@
# ACPC 2026: no SysRq key combinations (they can restart the machine)
kernel.sysrq = 0
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# Turns the contest restrictions off and on, for maintenance. Root only, so
# it is used through sudo from the administrator account (icpc).
#
# sudo contest-restrictions status
# sudo contest-restrictions report [SINCE] (refused attempts, default: today)
# sudo contest-restrictions off [all|firewall|usb|network]
# sudo contest-restrictions on [all|firewall|usb|network]
#
# firewall firewall, web proxy, DNS filter (/disable_firewall.sh, /enable_firewall.sh)
# usb USBGuard, USB disks and phones for the team account
# network network settings (Wi-Fi, hotspot, ...) for the team account
#
# "off" stays in effect after a reboot, until "on".
set -u
DISABLED=/etc/contest-restrictions/disabled
USB_FILES="/etc/polkit-1/rules.d/10-acpc-no-removable-media.rules /etc/udev/rules.d/72-acpc-no-phone-access.rules"
NETWORK_FILES="/etc/polkit-1/rules.d/10-acpc-no-network-changes.rules"
usage() {
sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
exit 2
}
[ "$(id -u)" -eq 0 ] || { echo "Run it with sudo, from the administrator account." >&2; exit 1; }
mkdir -p "$DISABLED"
chmod 700 /etc/contest-restrictions
# Moves rule files out of the way (off) or back (on)
move_rules() {
action=$1
shift
for f in "$@"; do
if [ "$action" = off ] && [ -f "$f" ]; then
mv "$f" "$DISABLED/"
elif [ "$action" = on ] && [ -f "$DISABLED/$(basename "$f")" ]; then
mv "$DISABLED/$(basename "$f")" "$f"
fi
done
}
firewall() {
if [ "$1" = off ]; then
/disable_firewall.sh
else
/enable_firewall.sh
/refresh_rules.sh
fi
}
usb() {
if [ "$1" = off ]; then
systemctl disable --now usbguard.service
# USBGuard leaves the controllers refusing new devices when it stops
for f in /sys/bus/usb/devices/usb*/authorized_default; do
[ -e "$f" ] && echo 1 > "$f"
done
move_rules off $USB_FILES
udevadm control --reload
echo "USB devices allowed (unplug and plug again a device refused before)."
echo "USB disks and phones can be used from the team account."
else
# Removable media mounted while the restriction was off
findmnt -rno TARGET | grep '^/run/media/' | while read -r m; do
umount "$m" && echo "Unmounted $m"
done
move_rules on $USB_FILES
udevadm control --reload
systemctl enable --now usbguard.service
echo "USB restrictions on. The devices plugged in right now stay allowed until they are unplugged."
fi
}
network() {
if [ "$1" = off ]; then
move_rules off $NETWORK_FILES
echo "The team account can change the network settings."
else
move_rules on $NETWORK_FILES
echo "Network settings restricted to the administrators."
fi
}
state() {
if [ "$1" = yes ]; then echo "on"; else echo "OFF"; fi
}
status() {
fw=no; systemctl is-active --quiet firewall.service && fw=yes
us=no; systemctl is-active --quiet usbguard.service && [ -f /etc/polkit-1/rules.d/10-acpc-no-removable-media.rules ] && us=yes
nw=no; [ -f /etc/polkit-1/rules.d/10-acpc-no-network-changes.rules ] && nw=yes
echo "firewall $(state $fw)"
echo "usb $(state $us)"
echo "network $(state $nw)"
}
# What the machine refused: web sites, DNS names, USB devices
report() {
since=${1:-today}
echo "== Web sites refused by the proxy (since $since)"
journalctl -t contest_proxy --since "$since" --no-pager -o cat 2>/dev/null \
| awk '$3 != "200" || $4 == "-" {print $2}' | sort | uniq -c | sort -rn | head -40
echo
echo "== DNS names refused by the filter"
# Names that Firefox, VS Code, the IDEs and Kattis pages look up on their
# own in the background
background='(^|\.)(mozilla\.(com|org|net)|firefox-portal-detection\.com|googleapis\.com|gvt1\.com|microsoft\.com|vscode-cdn\.net|vscode-unpkg\.net|exp-tas\.com|openh264\.org|ipv4only\.arpa|sentry\.io|cloudflare-dns\.com|jetbrains\.com|ubuntu\.com|canonical\.com|snapcraft\.io)$'
names=$(journalctl -u contest-dns --since "$since" --no-pager -o cat 2>/dev/null \
| sed -n 's/^config \(.*\) is NXDOMAIN$/\1/p')
printf '%s\n' "$names" | grep -v -E "$background" | grep . | sort | uniq -c | sort -rn | head -40
echo " ($(printf '%s\n' "$names" | grep -c -E "$background") lookups by Firefox, VS Code and the IDEs in the background, not listed)"
echo
echo "== USB devices rejected"
journalctl -u usbguard --since "$since" --no-pager -o short 2>/dev/null \
| grep "target.new='reject'" \
| sed -E 's/^([A-Za-z]+ +[0-9]+ [0-9:]+).* name "([^"]*)".*/\1 \2/' | tail -20
}
case "${1:-}" in
status) status ;;
report) report "${2:-today}" ;;
on|off)
what=${2:-all}
case "$what" in
all) firewall "$1"; usb "$1"; network "$1" ;;
firewall|usb|network) "$what" "$1" ;;
*) usage ;;
esac
echo
status
;;
*) usage ;;
esac
@@ -0,0 +1,41 @@
#!/bin/bash
# Runs as root (contest-restrictions-warning.service). While any contest
# restriction is off (after maintenance with contest-restrictions off), shows
# a red banner across the top of the screen, so that the proctors notice it.
# The banner has no title bar or close button, the team account cannot stop
# this process, and the banner comes back within seconds if its window is
# destroyed. It is redrawn every 15 seconds to stay above the other windows.
export DISPLAY=:0 XAUTHORITY=/var/run/lightdm/root/:0
off() {
! systemctl is-active --quiet firewall.service ||
! systemctl is-active --quiet usbguard.service ||
[ -n "$(ls -A /etc/contest-restrictions/disabled 2>/dev/null)" ]
}
show() {
width=$(xdpyinfo 2>/dev/null | awk '/dimensions/ {split($2, a, "x"); print a[1]}')
[ -n "$width" ] || return
xmessage -geometry "${width}x70+0+0" -buttons "" \
-xrm "*overrideRedirect: true" \
-fg white -bg red3 -fn "-*-helvetica-bold-r-*-*-34-*-*-*-*-*-*-*" \
"CONTEST RESTRICTIONS ARE OFF ON THIS MACHINE - CALL THE STAFF" >/dev/null 2>&1 &
pid=$!
shown=$(date +%s)
}
pid=""
shown=0
while true; do
if off && [ -e "$XAUTHORITY" ]; then
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
show
elif [ $(( $(date +%s) - shown )) -ge 15 ]; then
old=$pid
show
sleep 1
kill "$old" 2>/dev/null
fi
elif [ -n "$pid" ]; then
kill "$pid" 2>/dev/null
pid=""
fi
sleep 3
done
@@ -0,0 +1,11 @@
[Unit]
Description=Red banner on the screen while a contest restriction is off
After=display-manager.service
[Service]
ExecStart=/usr/local/sbin/contest-restrictions-warning
Restart=always
RestartSec=3
[Install]
WantedBy=graphical.target
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Manual staff command for permanently erasing the team account's files.
# Stops the desktop, deletes the home and team-owned files on the root
# filesystem, then recreates the home. No backup or undo.
# Never invoke from boot, login, a timer, cron, a playbook or automated tests.
# Run only after explicit staff authorization and verified backups.
# The --now argument is internal to the transient service after confirmation.
set -u
[ "$(id -u)" -eq 0 ] || { echo "Run this script as root (sudo)." >&2; exit 1; }
if [ "${1:-}" != --now ]; then
echo "The files of the team account will be deleted on this machine."
read -rp "Type RESET to continue: " answer
[ "$answer" = RESET ] || { echo "Cancelled."; exit 1; }
# The reset ends the desktop session, and with it this terminal if the
# script was started from the desktop. It runs as a separate service.
systemd-run --unit=reset-team-home --collect --quiet "$(readlink -f "$0")" --now
echo "Reset started. The screen restarts in a few seconds."
exit 0
fi
systemctl stop lightdm
loginctl terminate-user team 2>/dev/null
sleep 2
pkill -KILL -u team 2>/dev/null
sleep 1
for m in /home/team/.vscode/extensions /home/team/.vscode/extensions-root; do
umount "$m" 2>/dev/null
done
rm -rf --one-file-system /home/team
find / -xdev \( -path /proc -o -path /sys -o -path /dev \) -prune -o -user team -print0 2>/dev/null \
| xargs -0 -r rm -rf --one-file-system
mkdir /home/team
cp -a /etc/skel/. /home/team/
chown -R team:teams /home/team
chmod 750 /home/team
systemctl restart team-vscode-extensions.service
systemctl start lightdm
echo "The team account is clean."
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Wait for the clock before the desktop starts (at most 30 seconds)
After=chrony.service firewall.service contest-dns.service contest-proxy.service
Wants=chrony.service
Before=display-manager.service
[Service]
Type=oneshot
# Three checks 10 seconds apart, then the desktop starts anyway
ExecStart=-/usr/bin/chronyc waitsync 3 1
TimeoutStartSec=45
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Refresh the addresses of the allowlisted domains
After=firewall.service
[Service]
Type=oneshot
ExecStart=/refresh_rules.sh
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Keeps the address set of the time servers up to date, in one atomic
# nftables transaction. NTP pools answer with different addresses at each
# query, so the addresses chrony actually picked are added to those resolved
# here. Web traffic does not need addresses: the local proxy connects by name.
CONF=/etc/contest-firewall
TMP=$(mktemp)
mkdir -p /run/contest-firewall
# One run at a time (the timer and a manual run can overlap)
exec 9> /run/contest-firewall/lock
flock 9
entries() {
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$1" | grep -v '^$'
}
ntp_addresses() {
{
for name in $(entries "$CONF/ntp_servers.txt"); do
dig +short A "$name"
done
chronyc -n sources 2>/dev/null | awk '/^\^/ {print $2}'
} | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort -u
}
{
echo "flush set inet filter ntp_ips"
for ip in $(ntp_addresses); do
echo "add element inet filter ntp_ips { $ip }"
done
} > "$TMP"
# Apply atomically, then keep a copy of the active ruleset
nft -f "$TMP"
rm -f "$TMP"
nft list ruleset > /etc/nftables.rules
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Refresh the addresses of the allowlisted domains every 30 seconds
[Timer]
OnBootSec=0s
OnUnitActiveSec=30s
[Install]
WantedBy=timers.target
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Prepares the VS Code extensions of the team account at boot, as root, with
# the script of the ICPC image. The image runs that script at login through
# sudo, which every account could then use as root. The paths it mounts and
# changes are checked first: a symbolic link there could point anywhere.
set -eu
USER_NAME=team
home=$(getent passwd "$USER_NAME" | cut -d: -f6)
for path in "$home/.pki" "$home/.pki/nssdb" "$home/.vscode" \
"$home/.vscode/extensions" "$home/.vscode/extensions-work" \
"$home/.vscode/extensions-overlay" "$home/.vscode/extensions-root"; do
if [ -L "$path" ]; then
logger -t team-vscode-extensions "removing the symbolic link $path"
rm -f "$path"
fi
done
[ -L "$home" ] && { echo "$home is a symbolic link" >&2; exit 1; }
export SUDO_UID=$(id -u "$USER_NAME") SUDO_GID=$(id -g "$USER_NAME")
exec /usr/local/bin/vscode-extension-install
@@ -0,0 +1,12 @@
[Unit]
Description=VS Code extensions of the team account
After=local-fs.target
Before=display-manager.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/team-vscode-extensions
[Install]
WantedBy=multi-user.target
+7
View File
@@ -0,0 +1,7 @@
[Unit]
Description=Set the clock from the contest site if NTP does not work
After=firewall.service chrony.service
[Service]
Type=oneshot
ExecStart=/time_from_contest.sh
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Fallback for the clock. If chrony has no time server after two minutes of
# uptime (for instance when the campus network blocks NTP), the clock is set
# from the Date header of the contest site, which is accurate to the second.
CONF=/etc/contest-firewall
[ "$(cut -d. -f1 /proc/uptime)" -ge 120 ] || exit 0
if chronyc -n sources 2>/dev/null | grep -qE '^\^\*'; then
exit 0
fi
site=$(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$CONF/allowed_domains.txt" | grep -m1 .)
# -k: certificates cannot be checked while the clock is far off
date_header=$(curl -skI --max-time 10 "https://$site/" | tr -d '\r' | sed -n 's/^[Dd]ate: //p')
[ -n "$date_header" ] || { logger -t time_from_contest "no answer from $site"; exit 1; }
server=$(date -d "$date_header" +%s) || exit 1
now=$(date +%s)
offset=$((server - now))
if [ "${offset#-}" -ge 2 ]; then
# The header has whole seconds: aim for the middle of the second
date -s "@$server.5" >/dev/null
logger -t time_from_contest "chrony has no time server; clock set from $site (was off by $offset s)"
fi
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Check every minute that the clock has a time source
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target
+39
View File
@@ -0,0 +1,39 @@
- name: End of the build (SSH settings of the ICPC image, cleanup, power off)
hosts: all
become: yes
tasks:
# build.sh replaced these settings to log in during the build
- name: Restore the SSH settings of the ICPC image
copy:
dest: /etc/ssh/sshd_config.d/icpc.conf
mode: '0644'
content: |
PasswordAuthentication no
DenyGroups judges teams
- name: Remove the SSH settings of the build
file:
path: /etc/ssh/sshd_config.d/00-build.conf
state: absent
- name: Empty the package cache
command: apt-get clean
changed_when: false
# Last task, in one go because Ansible may be using sudo through the team
# account (first build) or the icpc account (next builds):
# - icpc, the administrator account, gets the new password
# - team, the account of the teams, gets its new password if one is given
# - team leaves the sudo group (the ICPC image gives it sudo with the
# public password "contest")
- name: Set the passwords, remove team from sudo and power off
shell: |
set -e
echo "icpc:$ADMIN_PASS" | chpasswd
if [ -n "$TEAM_PASS" ]; then echo "team:$TEAM_PASS" | chpasswd; fi
if id -nG team | grep -qw sudo; then gpasswd -d team sudo; fi
systemd-run --on-active=5 systemctl poweroff
environment:
ADMIN_PASS: "{{ admin_pass }}"
TEAM_PASS: "{{ team_pass | default('') }}"
no_log: true
+38
View File
@@ -0,0 +1,38 @@
- name: Firefox (home page and bookmarks of the contest)
hosts: all
become: yes
vars:
contest_url: https://atlantic-canada26.kattis.com
contest_id: atlantic-canada26
policies: /usr/lib/firefox/distribution/policies.json
# The bookmarks of the ICPC image point to http://contest, which is the
# contest server of the World Finals. They are replaced by the Kattis pages.
bookmarks:
- { Placement: toolbar, Title: "Contest", URL: "{{ contest_url }}/contests/{{ contest_id }}" }
- { Placement: toolbar, Title: "Problems", URL: "{{ contest_url }}/contests/{{ contest_id }}/problems" }
- { Placement: toolbar, Title: "Standings", URL: "{{ contest_url }}/contests/{{ contest_id }}/standings" }
- { Placement: toolbar, Title: "Contest Help (offline)", URL: "file:///usr/share/doc/acpc/help/index.html" }
- { Placement: toolbar, Title: "Language Documentation", URL: "https://devdocs/" }
- { Placement: toolbar, Title: "Technical Notes", URL: "file:///usr/share/doc/icpc/TechNotes.pdf" }
tasks:
- name: Set the home page and the bookmarks
shell: |
python3 - "{{ policies }}" <<'PY'
import json, sys
path = sys.argv[1]
data = json.load(open(path))
p = data.setdefault("policies", {})
before = json.dumps(data, sort_keys=True)
p["Bookmarks"] = json.loads('{{ bookmarks | to_json }}')
p["Homepage"] = {"Locked": True, "StartPage": "homepage-locked",
"URL": "{{ contest_url }}"}
# No extensions can be installed, and about:config is closed
p["ExtensionSettings"] = {"*": {"installation_mode": "blocked"}}
p["BlockAboutConfig"] = True
if json.dumps(data, sort_keys=True) != before:
json.dump(data, open(path, "w"), indent=2)
print("changed")
PY
register: ff
changed_when: "'changed' in ff.stdout"
+222
View File
@@ -0,0 +1,222 @@
- name: Contest firewall (nftables allowlist and local proxy)
hosts: all
become: yes
vars:
# Start the firewall at the end of the run. The current SSH session keeps
# working, but new inbound connections are dropped once it is active.
start_firewall: true
timezone: America/Moncton
dns_servers: "{{ lookup('file', 'dns_servers.txt').splitlines() | map('regex_replace', '#.*$', '') | map('trim') | reject('equalto', '') | list }}"
ntp_servers: "{{ lookup('file', 'ntp_servers.txt').splitlines() | map('regex_replace', '#.*$', '') | map('trim') | reject('equalto', '') | list }}"
tasks:
- name: Install nftables, dig, chrony and the nginx stream module
apt:
name:
- nftables
- dnsutils
- chrony
- nginx
- libnginx-mod-stream
- dnsmasq-base
state: present
update_cache: yes
cache_valid_time: 3600
- name: Create the account of the proxy
user:
name: contest-proxy
system: yes
create_home: no
shell: /usr/sbin/nologin
- name: Create the configuration directory
file:
path: /etc/contest-firewall
state: directory
mode: '0755'
- name: Copy the domain, DNS and NTP lists
copy:
src: "{{ item }}"
dest: "/etc/contest-firewall/{{ item }}"
mode: '0644'
loop:
- allowed_domains.txt
- dns_servers.txt
- ntp_servers.txt
- name: Copy the configuration of the proxy
copy:
src: files/contest-proxy.conf
dest: /etc/contest-firewall/contest-proxy.conf
mode: '0644'
- name: Copy the test script
copy:
src: test_firewall.sh
dest: /test_firewall.sh
mode: '0755'
- name: Copy the firewall scripts
copy:
src: "files/{{ item }}"
dest: "/{{ item }}"
mode: '0500'
loop:
- firewall.sh
- refresh_rules.sh
- enable_firewall.sh
- disable_firewall.sh
- name: Copy the systemd units
copy:
src: "files/{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: '0644'
loop:
- firewall.service
- refresh_rules.service
- refresh_rules.timer
- contest-proxy.service
- contest-dns.service
# The proxy reads the site name, which encrypted client hello (ECH) hides
- name: Turn off encrypted client hello in Firefox
shell: |
python3 - /usr/lib/firefox/distribution/policies.json <<'PY'
import json, os, sys
path = sys.argv[1]
if not os.path.exists(path):
sys.exit(0)
data = json.load(open(path))
prefs = data.setdefault("policies", {}).setdefault("Preferences", {})
before = json.dumps(data, sort_keys=True)
for name in ("network.dns.echconfig.enabled", "network.dns.http3_echconfig.enabled"):
prefs[name] = {"Value": False, "Status": "locked"}
if json.dumps(data, sort_keys=True) != before:
json.dump(data, open(path, "w"), indent=2)
print("changed")
PY
register: ech
changed_when: "'changed' in ech.stdout"
- name: Configure DNS servers in resolved.conf
lineinfile:
path: /etc/systemd/resolved.conf
regexp: '^#?DNS='
line: "DNS={{ dns_servers | join(' ') }}"
state: present
- name: Disable the DNS stub listener
lineinfile:
path: /etc/systemd/resolved.conf
regexp: '^#?DNSStubListener='
line: 'DNSStubListener=no'
state: present
- name: Configure chrony NTP servers
blockinfile:
path: /etc/chrony/chrony.conf
marker: "# {mark} CONTEST FIREWALL NTP SERVERS"
block: |
{% for ntp in ntp_servers %}
pool {{ ntp }} iburst
{% endfor %}
insertbefore: BOF
- name: Remove the default NTP pools from chrony.conf
lineinfile:
path: /etc/chrony/chrony.conf
regexp: '^(pool|server)\s+(?!.*iburst$).*'
state: absent
# Ubuntu 26.04 adds NTS time sources that the firewall blocks, and chrony
# does not use the plain NTP servers while those sources are configured
- name: Disable the Ubuntu NTS time sources
command: mv /etc/chrony/sources.d/ubuntu-ntp-pools.sources /etc/chrony/sources.d/ubuntu-ntp-pools.sources.disabled
args:
removes: /etc/chrony/sources.d/ubuntu-ntp-pools.sources
# The contest machines may also run Windows, which keeps the hardware
# clock in local time. chrony must not write its time into that clock.
- name: Do not write the time into the hardware clock
lineinfile:
path: /etc/chrony/chrony.conf
regexp: '^rtcsync'
state: absent
- name: Step the clock at boot whatever the offset
lineinfile:
path: /etc/chrony/chrony.conf
regexp: '^makestep'
line: makestep 1 3
- name: Copy the clock fallback (time from the contest site if NTP fails)
copy:
src: "files/time/{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
loop:
- { src: time_from_contest.sh, dest: /time_from_contest.sh, mode: '0500' }
- { src: time_from_contest.service, dest: /etc/systemd/system/time_from_contest.service, mode: '0644' }
- { src: time_from_contest.timer, dest: /etc/systemd/system/time_from_contest.timer, mode: '0644' }
- name: Enable NTP synchronization
command: timedatectl set-ntp true
changed_when: false
- name: Set the timezone
file:
src: "/usr/share/zoneinfo/{{ timezone }}"
dest: /etc/localtime
state: link
force: yes
- name: Block open.kattis.com (past submissions) via the hosts file
lineinfile:
path: /etc/hosts
line: "127.0.0.1\topen.kattis.com"
state: present
- name: Restart systemd-resolved
systemd:
name: systemd-resolved
state: restarted
- name: Enable and restart chrony
systemd:
name: chrony
enabled: yes
state: restarted
- name: Reload systemd
systemd:
daemon_reload: yes
- name: Enable the firewall and refresh services
systemd:
name: "{{ item }}"
enabled: yes
loop:
- firewall.service
- refresh_rules.service
- name: Enable the clock fallback
systemd:
name: time_from_contest.timer
enabled: yes
- name: Enable and start the refresh timer
systemd:
name: refresh_rules.timer
enabled: yes
state: started
# Last task, launched in the background: once the firewall is active,
# Ansible cannot open a new SSH connection to collect a result.
- name: Start the firewall
shell: sleep 2 && systemctl start firewall.service && /refresh_rules.sh
async: 60
poll: 0
when: start_firewall | bool
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# Installs the contest firewall without Ansible, on the booted contest image
# (Ubuntu 26.04, as in the ICPC image). Run it as root from this directory.
#
# sudo ./install_firewall.sh # install, the firewall starts at the next boot
# sudo ./install_firewall.sh --start # install and start it now
set -euo pipefail
cd "$(dirname "$0")"
if [ "$(id -u)" -ne 0 ]; then
echo "Run this script as root (sudo)." >&2
exit 1
fi
START=no
[ "${1:-}" = "--start" ] && START=yes
TIMEZONE=${TIMEZONE:-America/Moncton}
entries() {
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$1" | grep -v '^$'
}
echo "Installing nftables, dig, chrony and the nginx stream module"
apt-get update
apt-get install -y nftables dnsutils chrony nginx libnginx-mod-stream dnsmasq-base
id contest-proxy >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin contest-proxy
echo "Copying the lists, scripts and systemd units"
install -d -m 0755 /etc/contest-firewall
install -m 0644 allowed_domains.txt dns_servers.txt ntp_servers.txt files/contest-proxy.conf /etc/contest-firewall/
install -m 0500 files/firewall.sh files/refresh_rules.sh files/enable_firewall.sh files/disable_firewall.sh /
install -m 0755 test_firewall.sh /
install -m 0644 files/firewall.service files/refresh_rules.service files/refresh_rules.timer files/contest-proxy.service files/contest-dns.service /etc/systemd/system/
echo "Configuring DNS (systemd-resolved)"
dns_list=$(entries dns_servers.txt | tr '\n' ' ' | sed 's/ $//')
if grep -qE '^#?DNS=' /etc/systemd/resolved.conf; then
sed -i -E "s|^#?DNS=.*|DNS=${dns_list}|" /etc/systemd/resolved.conf
else
echo "DNS=${dns_list}" >> /etc/systemd/resolved.conf
fi
if grep -qE '^#?DNSStubListener=' /etc/systemd/resolved.conf; then
sed -i -E 's|^#?DNSStubListener=.*|DNSStubListener=no|' /etc/systemd/resolved.conf
else
echo "DNSStubListener=no" >> /etc/systemd/resolved.conf
fi
echo "Configuring NTP (chrony) and the timezone"
sed -i -E '/^# (BEGIN|END) CONTEST FIREWALL NTP SERVERS$/,/^# END CONTEST FIREWALL NTP SERVERS$/d' /etc/chrony/chrony.conf
sed -i -E '/^(pool|server)[[:space:]]/d' /etc/chrony/chrony.conf
{
echo "# BEGIN CONTEST FIREWALL NTP SERVERS"
for ntp in $(entries ntp_servers.txt); do
echo "pool ${ntp} iburst"
done
echo "# END CONTEST FIREWALL NTP SERVERS"
cat /etc/chrony/chrony.conf
} > /etc/chrony/chrony.conf.new
mv /etc/chrony/chrony.conf.new /etc/chrony/chrony.conf
# Ubuntu 26.04 adds NTS time sources that the firewall blocks, and chrony
# does not use the plain NTP servers while those sources are configured
if [ -f /etc/chrony/sources.d/ubuntu-ntp-pools.sources ]; then
mv /etc/chrony/sources.d/ubuntu-ntp-pools.sources /etc/chrony/sources.d/ubuntu-ntp-pools.sources.disabled
fi
# The contest machines may also run Windows, which keeps the hardware clock
# in local time: chrony must not write its time into that clock
sed -i -E '/^rtcsync/d' /etc/chrony/chrony.conf
grep -q '^makestep' /etc/chrony/chrony.conf || echo 'makestep 1 3' >> /etc/chrony/chrony.conf
# Fallback: time from the contest site if NTP does not work
install -m 0500 files/time/time_from_contest.sh /
install -m 0644 files/time/time_from_contest.service files/time/time_from_contest.timer /etc/systemd/system/
ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
echo "Blocking open.kattis.com"
grep -q 'open\.kattis\.com' /etc/hosts || printf '127.0.0.1\topen.kattis.com\n' >> /etc/hosts
# Firefox must send the real site name, which encrypted client hello (ECH) hides
POLICIES=/usr/lib/firefox/distribution/policies.json
if [ -f "$POLICIES" ]; then
echo "Turning off encrypted client hello in Firefox"
python3 - "$POLICIES" <<'PY'
import json, sys
path = sys.argv[1]
data = json.load(open(path))
prefs = data.setdefault("policies", {}).setdefault("Preferences", {})
for name in ("network.dns.echconfig.enabled", "network.dns.http3_echconfig.enabled"):
prefs[name] = {"Value": False, "Status": "locked"}
json.dump(data, open(path, "w"), indent=2)
PY
fi
echo "Restarting services"
systemctl restart systemd-resolved
systemctl enable chrony
systemctl restart chrony
timedatectl set-ntp true || true
systemctl daemon-reload
systemctl enable firewall.service refresh_rules.service refresh_rules.timer time_from_contest.timer
systemctl start refresh_rules.timer
if [ "$START" = yes ]; then
systemctl start firewall.service
/refresh_rules.sh
echo "Firewall installed and active."
else
echo "Firewall installed. It will start at the next boot (or run: sudo /enable_firewall.sh)."
fi
+126
View File
@@ -0,0 +1,126 @@
- name: Compilers of the contest (same versions and options as Kattis)
hosts: all
become: yes
vars:
# From atlantic-canada26.kattis.com/contests/atlantic-canada26/languages
# C gcc-15 15.2.0
# C++ g++-15 15.2.0
# Java javac / java 25.0.4.1
# Python 3 pypy3 7.3.23 (Python 3.11.15)
# All these versions are present in the base ICPC image. The download
# block also corrects an existing build with a different Java version.
# Checksums are from the ICPC Ubuntu repository's package metadata.
jdk_version: "25.0.4.1+1-1~26.04.4"
jdk_packages:
- { name: openjdk-25-jre-headless, arch: amd64, sha256: fc2084482511d56ccb4c33746fe693313a5c0fdcc9a0442e8227e6ba948006d1 }
- { name: openjdk-25-jre, arch: amd64, sha256: 743a58cd138ccc58c1fbdfd26938a0c75edf21cebe1b938f09f37f4a2fb94866 }
- { name: openjdk-25-jdk-headless, arch: amd64, sha256: 7561b5bccd2644d8a2da5dcdfb597a40234f481e098bf785d1a8c8f90202e655 }
- { name: openjdk-25-jdk, arch: amd64, sha256: 9259712883b6ee039b86b7b08292cfb7747bd030e4ee73c66c2517f95999a861 }
- { name: openjdk-25-source, arch: all, sha256: d4949263fd30053bcb96b247bc6971b8b3d2a7d8e0cfec4c106095f52c3948a9 }
jdk_cache: /var/cache/acpc-jdk
tasks:
- name: Make sure GCC 15 is installed
apt:
name: [gcc-15, g++-15]
state: present
- name: Check the installed Java packages
command:
argv:
- dpkg-query
- -W
- '-f=${Version} ${db:Status-Status}'
- '{{ item.name }}'
loop: '{{ jdk_packages }}'
register: jdk_installed
changed_when: false
failed_when: false
- name: Install the contest Java version if it differs
when: (jdk_installed.results | map(attribute='stdout') | unique | list) != [jdk_version ~ ' installed']
block:
- name: Create the download directory
file:
path: "{{ jdk_cache }}"
state: directory
mode: '0755'
- name: Download the Java 25.0.4.1 packages
get_url:
url: "https://sysopspackages.icpc.global/ubuntu/resolute/pool/main/o/openjdk-25/{{ item.name }}_{{ jdk_version }}_{{ item.arch }}.deb"
dest: "{{ jdk_cache }}/{{ item.name }}_{{ jdk_version }}_{{ item.arch }}.deb"
checksum: "sha256:{{ item.sha256 }}"
mode: '0644'
loop: "{{ jdk_packages }}"
# Their exact-version dependencies must be updated together.
- name: Install the checked Java packages together
command: >-
dpkg --force-confdef --force-confold --install
{% for package in jdk_packages %}
{{ jdk_cache }}/{{ package.name }}_{{ jdk_version }}_{{ package.arch }}.deb
{% endfor %}
- name: Remove the downloaded packages
file:
path: "{{ jdk_cache }}"
state: absent
- name: Keep Java at 25.0.4.1 (no automatic upgrade)
dpkg_selections:
name: "{{ item.name }}"
selection: hold
loop: "{{ jdk_packages }}"
- name: Install the compile and run commands
copy:
src: "files/languages/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
mode: '0755'
loop:
- compilec
- compilecpp
- compilejava
- compilepython3
- runc
- runcpp
- runjava
- runpython3
# Names used by the ICPC technical notes
- name: Keep compilegcc and compileg++ as other names
file:
src: "/usr/local/bin/{{ item.src }}"
dest: "/usr/local/bin/{{ item.dest }}"
state: link
force: yes
loop:
- { src: compilec, dest: compilegcc }
- { src: compilecpp, dest: compileg++ }
- name: Read the versions
shell: |
gcc-15 -dumpfullversion
g++-15 -dumpfullversion
java -version 2>&1 | head -1
javac -version 2>&1
pypy3 -c 'import sys; print(sys.pypy_version_info[:3], sys.version_info[:3])'
register: versions
changed_when: false
- name: Check the versions against Kattis
assert:
that:
- versions.stdout_lines[0] == "15.2.0"
- versions.stdout_lines[1] == "15.2.0"
- "'\"25.0.4.1\"' in versions.stdout_lines[2]"
- versions.stdout_lines[3] == "javac 25.0.4.1"
- versions.stdout_lines[4] == "(7, 3, 23) (3, 11, 15)"
fail_msg: "{{ versions.stdout_lines }}"
- name: Install the technical notes for the teams
copy:
src: docs/TechNotes.pdf
dest: /usr/share/doc/icpc/TechNotes.pdf
mode: '0644'
+183
View File
@@ -0,0 +1,183 @@
- name: Lockdown (AI completion, USB devices, network settings, printers, clock at login)
hosts: all
become: yes
tasks:
# IntelliJ IDEA and PyCharm ship local AI models for code completion
# (Full Line), which work without any network. The models and the local
# inference server are removed. The plugin code stays, because other
# parts of PyCharm depend on it.
- name: Find the local AI completion models of the JetBrains IDEs
find:
paths: [/opt/intellij-idea/plugins/fullLine, /opt/pycharm/plugins/fullLine]
patterns: ["full-line-model-*", "full-line-native-server-*"]
register: fullline
- name: Remove them
file:
path: "{{ item.path }}"
state: absent
loop: "{{ fullline.files }}"
loop_control:
label: "{{ item.path }}"
- name: Check that no model is left
shell: find /opt -xdev \( -name 'full-line-model-*' -o -name 'full-line-native-server-*' -o -iname '*.gguf' \) | head -5
register: models_left
changed_when: false
failed_when: models_left.stdout | length > 0
# udisks lets the desktop user mount USB sticks without a password
- name: Refuse USB disks and disk images to the teams (polkit)
copy:
src: files/lockdown/10-acpc-no-removable-media.rules
dest: /etc/polkit-1/rules.d/10-acpc-no-removable-media.rules
mode: '0644'
- name: Refuse direct access to phones and cameras (MTP, PTP)
copy:
src: files/lockdown/72-acpc-no-phone-access.rules
dest: /etc/udev/rules.d/72-acpc-no-phone-access.rules
mode: '0644'
# USBGuard: the devices present at boot (the stick itself, keyboard, mouse)
# are allowed, any device plugged in later is rejected. This also stops
# a device that types prepared code while posing as a keyboard.
# An administrator allows a replacement keyboard with
# "sudo usbguard list-devices" and "sudo usbguard allow-device ID".
- name: Install USBGuard
apt:
name: usbguard
state: present
- name: Allow the devices present at boot
lineinfile:
path: /etc/usbguard/usbguard-daemon.conf
regexp: '^PresentDevicePolicy='
line: PresentDevicePolicy=allow
- name: Reject the devices plugged in later
lineinfile:
path: /etc/usbguard/usbguard-daemon.conf
regexp: '^ImplicitPolicyTarget='
line: ImplicitPolicyTarget=reject
# No rule list: it would describe the devices of the build machine
- name: Empty rule list
copy:
dest: /etc/usbguard/rules.conf
content: ""
mode: '0600'
- name: Enable USBGuard
systemd:
name: usbguard.service
enabled: yes
- name: Refuse network changes to the teams (polkit)
copy:
src: files/lockdown/10-acpc-no-network-changes.rules
dest: /etc/polkit-1/rules.d/10-acpc-no-network-changes.rules
mode: '0644'
# chrony steps the clock a few seconds after boot. Without this, the
# desktop could start with the clock of the machine and show it for a
# while.
- name: Wait for the clock before the desktop starts
copy:
src: files/lockdown/wait-for-time.service
dest: /etc/systemd/system/wait-for-time.service
mode: '0644'
- name: Enable it
systemd:
name: wait-for-time.service
enabled: yes
daemon_reload: yes
# A restart would let USBGuard accept a device plugged in before it, so the
# teams may not restart, power off or suspend the machine
- name: Refuse restart, power off and suspend to the teams (polkit)
copy:
src: files/lockdown/10-acpc-no-power-actions.rules
dest: /etc/polkit-1/rules.d/10-acpc-no-power-actions.rules
mode: '0644'
- name: No restart with Ctrl+Alt+Del
systemd:
name: ctrl-alt-del.target
masked: yes
- name: No SysRq key combinations
copy:
src: files/lockdown/90-acpc-sysrq.conf
dest: /etc/sysctl.d/90-acpc-sysrq.conf
mode: '0644'
- name: Power, reboot and suspend keys do nothing
block:
- file:
path: /etc/systemd/logind.conf.d
state: directory
mode: '0755'
- copy:
src: files/lockdown/90-acpc-keys.conf
dest: /etc/systemd/logind.conf.d/90-acpc-keys.conf
mode: '0644'
# The image must not change during maintenance with the firewall off:
# an upgrade could change the versions of the compilers (Kattis)
- name: No automatic package updates
systemd:
name: "{{ item }}"
enabled: no
masked: yes
loop:
- apt-daily.timer
- apt-daily-upgrade.timer
- unattended-upgrades.service
# The lpadmin group administers the printers and can write in
# /usr/share/ppd/custom. Printing does not need it.
- name: Remove team from the lpadmin group
command: gpasswd -d team lpadmin
register: lpadmin
changed_when: lpadmin.rc == 0
failed_when: false
# Root only: the administrator runs it with sudo from the icpc account
- name: Install the command that turns the restrictions off and on
copy:
src: files/lockdown/contest-restrictions
dest: /usr/local/sbin/contest-restrictions
mode: '0700'
# Runs as root, so the team account cannot stop it
- name: Show a red banner on the screen while a restriction is off
copy:
src: "files/lockdown/{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
loop:
- { src: contest-restrictions-warning, dest: /usr/local/sbin/contest-restrictions-warning, mode: '0700' }
- { src: contest-restrictions-warning.service, dest: /etc/systemd/system/contest-restrictions-warning.service, mode: '0644' }
- name: Enable the banner service
systemd:
name: contest-restrictions-warning.service
enabled: yes
daemon_reload: yes
- name: Create the folder of the restrictions turned off (root only)
file:
path: "{{ item }}"
state: directory
mode: '0700'
loop:
- /etc/contest-restrictions
- /etc/contest-restrictions/disabled
- name: Install the reset script of the team account
copy:
src: files/lockdown/reset_team_home.sh
dest: /reset_team_home.sh
mode: '0500'
+5
View File
@@ -0,0 +1,5 @@
# Time servers (UDP port 123). Replace them with your campus servers if needed.
0.pool.ntp.org
1.pool.ntp.org
2.pool.ntp.org
3.pool.ntp.org
Executable
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# Boots the built image in a window, to try it as a team would. Changes made
# in this window are discarded when it closes (snapshot mode).
#
# ./run_vm.sh # build/acpc2026.qcow2
# ./run_vm.sh path/to/image.img # another image
# DISPLAY_OPT="-display none -vnc 127.0.0.1:7" ./run_vm.sh # without a window
cd "$(dirname "$0")"
IMG=${1:-build/acpc2026.qcow2}
[ -f "$IMG" ] || { echo "Image not found: $IMG (run ./build.sh first)" >&2; exit 1; }
FORMAT=$(qemu-img info --output=json "$IMG" | python3 -I -c 'import json, sys; print(json.load(sys.stdin)["format"])')
for dir in /usr/share/edk2/x64 /usr/share/OVMF /usr/share/edk2/ovmf; do
for code in OVMF_CODE.4m.fd OVMF_CODE_4M.fd OVMF_CODE.fd; do
[ -f "$dir/$code" ] && { OVMF_CODE=$dir/$code; OVMF_VARS=$dir/${code/CODE/VARS}; break 2; }
done
done
[ -n "${OVMF_CODE:-}" ] || { echo "UEFI firmware (OVMF) not found" >&2; exit 1; }
VARS=$(mktemp)
cp "$OVMF_VARS" "$VARS"
trap 'rm -f "$VARS"' EXIT
# shellcheck disable=SC2086
qemu-system-x86_64 -name acpc2026-try -machine q35,accel=kvm -cpu host -m 4096 -smp 4 \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$VARS" \
-drive file="$IMG",format="$FORMAT",if=virtio,snapshot=on \
-netdev user,id=net0 -device virtio-net-pci,netdev=net0 \
-vga virtio ${DISPLAY_OPT:--display gtk} ${QEMU_EXTRA:-}
+9
View File
@@ -0,0 +1,9 @@
# Full configuration of the contest image, run by build.sh
- import_playbook: accounts.yml
- import_playbook: languages.yml
- import_playbook: firefox.yml
- import_playbook: desktop.yml
- import_playbook: docs.yml
- import_playbook: firewall.yml
- import_playbook: lockdown.yml
- import_playbook: finish.yml
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# Quick check of the contest firewall, to run on a contest machine once the
# firewall is active: allowed sites must answer, every other site must fail,
# also when a program forces the address of the contest site for another name.
CONTEST_URL=${CONTEST_URL:-https://atlantic-canada26.kattis.com}
CONTEST_HOST=${CONTEST_URL#https://}
CONTEST_HOST=${CONTEST_HOST%%/*}
fail=0
report() {
if [ "$2" = "$3" ]; then
echo "OK $1 is $2"
else
echo "PROBLEM $1 is $2 (expected: $3)"
fail=1
fi
}
check() {
expected=$1
url=$2
if curl -s -o /dev/null --max-time 8 "$url"; then result=reachable; else result=blocked; fi
report "$url" $result "$expected"
}
# Another site reached through the address of the contest site (Cloudflare)
check_through_contest() {
host=$1
if curl -s -o /dev/null --max-time 8 --resolve "$host:443:$contest_ip" "https://$host/"; then
result=reachable
else
result=blocked
fi
report "https://$host/ through the address of $CONTEST_HOST" $result blocked
}
check reachable "$CONTEST_URL"
check reachable https://status.kattis.com
check reachable https://devdocs/
check blocked https://open.kattis.com
check blocked https://www.google.com
check blocked https://github.com
check blocked https://chatgpt.com
contest_ip=$(getent ahostsv4 "$CONTEST_HOST" | awk 'NR == 1 {print $1}')
if [ -n "$contest_ip" ]; then
check_through_contest open.kattis.com
check_through_contest pastebin.com
check_through_contest discord.com
else
echo "PROBLEM $CONTEST_HOST does not resolve"
fail=1
fi
# DNS: only the allowed names get an answer, and the DNS resolvers outside
# cannot be reached directly (no data exchanged through DNS)
if [ -n "$(dig +short +time=3 +tries=1 TXT example.com 2>/dev/null)" ]; then result=reachable; else result=blocked; fi
report "DNS for other names (TXT example.com)" $result blocked
if dig +time=3 +tries=1 example.com @1.1.1.1 >/dev/null 2>&1; then result=reachable; else result=blocked; fi
report "the DNS resolver 1.1.1.1 directly" $result blocked
echo
for unit in contest-proxy contest-dns; do
if systemctl is-active --quiet $unit.service; then
echo "OK $unit is running"
else
echo "PROBLEM $unit.service is not running"
fail=1
fi
done
if chronyc -n sources 2>/dev/null | grep -qE '^\^\*'; then
echo "OK the clock is synchronized (NTP)"
else
echo "PROBLEM the clock is not synchronized yet (check again in a minute)"
fail=1
fi
exit $fail
+46
View File
@@ -0,0 +1,46 @@
# Testing a build
These tools start a throwaway copy of `build/acpc2026.qcow2` in QEMU and drive it from the build machine, over SSH and through the virtual keyboard, so that the image can be checked as a team and an administrator would use it. The built image itself is not modified. They need `qemu-storage-daemon`, `debugfs`, `sshpass` and Python with Pillow on the build machine, and the administrator password in `ADMIN_PASS`.
```
export ADMIN_PASS='PASSWORD'
tests/check_vm.sh start --wrong-clock --usb
tests/feature_check.sh
tests/technotes_check.sh
tests/ide_check.sh
tests/check_vm.sh stop
```
`feature_check.sh` runs, as root in the virtual machine, one check for each feature of the playbooks. `technotes_check.sh` runs, from the team account, the statements of the technical notes that can be checked on the machine. Both write their results to `test_report/` and end with the number of failed checks, which should be 0. When a playbook or the notes change, add the matching check.
| Tool | Use |
| --- | --- |
| `check_vm.sh start [--wrong-clock] [--usb]` | boots the copy (`tests/work/`) with SSH open for the tests and waits for the team desktop. `--wrong-clock` starts with the hardware clock 3 hours behind, `--usb` adds a USB controller |
| `check_vm.sh stop` | stops the virtual machine and deletes the copy |
| `vssh.sh team\|icpc 'command'` | runs a command over SSH |
| `gui.sh 'program arguments'` | starts a program on the team desktop |
| `key.sh 'text{{ret}}'` | types on the keyboard of the virtual machine. Special keys use the QEMU names, as in `{{esc}}`, `{{ctrl-s}}` and `{{f5}}`, and `key.sh @file` types a file |
| `shot.sh NAME` | saves a screenshot as `test_report/NAME.png` |
| `add_test_tools.sh` | installs `xdotool` in the copy only, for `click.sh` |
| `click.sh X Y [right\|double]` | clicks on the team desktop at a screen position, read on a screenshot (1280 by 800 pixels) |
| `usb.sh plug\|unplug` | plugs a small USB stick into a copy started with `--usb` |
| `make_report.py` | builds `test_report/index.html` from the screenshots, `test_report/captions.txt` (one `NAME \| caption` line per screenshot), `test_report/intro.txt` and the two checklists |
| `ide_check.sh` | verifies the installed IDE commands, C23/C++23, static linking, Java VM options, PyPy, paths with spaces, compiler probes and failed-build handling |
| `make_ide_report.py` | builds the separate, self-contained `test_report/ide-config/index.html` from reviewed screenshots and `results.json` |
A full test goes through the following steps, with a screenshot of each.
1. Boot with the wrong clock. The team desktop appears with the right time.
2. Open Firefox from the Applications menu, then the contest, standings, problems, login and status pages, and a few blocked sites.
3. Write a short program in each language in a different editor, then compile and run it in a terminal with the `compile` and `run` commands. `submit` prints the address of the problems on Kattis.
4. Run `/test_firewall.sh` in the team terminal.
5. Try to get around the restrictions from the team terminal (sudo, reboot, Wi-Fi, reading the administration scripts, a USB stick, a forced address, DNS).
6. Build and run a program in each language of each IDE, with the input typed in the IDE, and check which compiler or interpreter the IDE used. Cover all 19 combinations: Geany and VS Code and Eclipse (C, C++, Java, Python), Code::Blocks (C, C++), CLion (C, C++, Python), IntelliJ IDEA (Java), and PyCharm (Python). Use the actions in the technical notes; the ordinary Java run buttons in VS Code and Eclipse use ECJ, so test their supplied ACPC actions to verify javac. The fixtures in `ide_samples/` assert the language standards and runtime versions, exercise C++23 `std::println`, and check Java's seven VM options. Enter `3000000000 4`; each program must print `SUM = 3000000004` and exit successfully.
7. In the team terminal, run `su - icpc`, then `contest-restrictions status`, `off usb` (red banner), plug a USB stick, `report` and `on usb`. Check that the restrictions are active and the banner has disappeared.
8. Open **Contest Help (offline)** from the Contest menu. Disconnect the VM network with `python3 -I tests/monitor.py tests/work/monitor.sock 'set_link net0 off'`. Reload the page, search for `EOF`, follow an article link and open the local Technical Notes PDF. Use `key.sh` and `shot.sh` while SSH is unavailable. Restore the connection with `python3 -I tests/monitor.py tests/work/monitor.sock 'set_link net0 on'` before continuing other checks.
Do not run `/reset_team_home.sh` as part of this procedure or any automated test. It permanently erases team files and is reserved for an explicitly authorized manual staff action. The feature checks inspect its installation without executing it.
Keyboard input goes to the window that has the focus, so close dialogs and other windows before typing. First-launch windows depend on what was used before (PyCharm offers to import the settings of VS Code once VS Code has run), so the IDEs are worth testing on a fresh copy as well. The JetBrains IDEs take up to a minute to open in the virtual machine.
For the IDE report, record each reviewed result in `test_report/ide-config/results.json`: a `cases` list with `id`, `ide`, `language`, `action`, `status`, `evidence`, and `screenshots` (each with `file` and `caption`), plus `verdict`, `build`, `notes`, and `sources` (each with `title` and `url`). Keep the original report and screenshots. Run the final graphical checks on a fresh Ansible build without manually repairing compiler settings. Test fixtures may be copied into the VM; compile and run them through the actual IDE interface and inspect every selected screenshot.
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Installs xdotool in the throwaway test VM (not in the built image), for
# mouse clicks with tests/click.sh. The firewall is turned off during the
# installation and back on afterwards. ADMIN_PASS=... tests/add_test_tools.sh
cd "$(dirname "$0")/.."
tests/vssh.sh icpc "echo '${ADMIN_PASS:?set ADMIN_PASS}' | sudo -S -p '' bash -c '
command -v xdotool >/dev/null && exit 0
contest-restrictions off firewall >/dev/null
apt-get install -y -qq xdotool >/dev/null 2>&1
contest-restrictions on firewall >/dev/null
/refresh_rules.sh
command -v xdotool'"
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
# Test virtual machine for the built image (build/acpc2026.qcow2).
#
# tests/check_vm.sh start [--wrong-clock] [--usb] boot a throwaway copy
# tests/check_vm.sh stop power it off, delete the copy
#
# The copy (tests/work/check.qcow2) opens SSH for the tests: password login,
# no DenyGroups, and the firewall accepts tcp 22. The built image itself is
# not modified. --wrong-clock starts with the hardware clock 3 hours behind
# (as on a lab machine running Windows), --usb adds a USB controller so that
# USB devices can be plugged in with tests/usb.sh.
set -eu
cd "$(dirname "$0")/.."
W=tests/work
MON=$W/monitor.sock
BUILT=$(realpath build/acpc2026.qcow2)
running() { pgrep -f "^qemu-system-x86_64 -name acpc2026-check" >/dev/null; }
case "${1:-}" in
start)
shift
running && { echo "The test VM is already running." >&2; exit 1; }
mkdir -p "$W"
qemu-img create -q -f qcow2 -b "$BUILT" -F qcow2 "$W/check.qcow2"
for d in /usr/share/edk2/x64 /usr/share/OVMF /usr/share/edk2/ovmf; do
for c in OVMF_CODE.4m.fd OVMF_CODE_4M.fd OVMF_CODE.fd; do
[ -f "$d/$c" ] && { CODE=$d/$c; VARS=$d/${c/CODE/VARS}; break 2; }
done
done
cp "$VARS" "$W/vars.fd"
# Open SSH in the copy, offline
touch "$W/fuse.raw"
qemu-storage-daemon --blockdev driver=file,node-name=f,filename="$W/check.qcow2" \
--blockdev driver=qcow2,node-name=q,file=f \
--export type=fuse,id=e,node-name=q,mountpoint="$W/fuse.raw",writable=on > "$W/qsd.log" 2>&1 &
for _ in $(seq 1 20); do mountpoint -q "$W/fuse.raw" && break; sleep 0.5; done
start=$(sfdisk -J "$W/fuse.raw" | python3 -I -c 'import json,sys; p=json.load(sys.stdin)["partitiontable"]["partitions"]; print(max(p,key=lambda x:x["size"])["start"])')
D="$W/fuse.raw?offset=$((start * 512))"
printf 'PasswordAuthentication yes\n' > "$W/00-test.conf"
w() { debugfs -w -R "$1" "$D" >/dev/null 2>&1 || true; }
w "rm /etc/ssh/sshd_config.d/icpc.conf"
w "write $W/00-test.conf /etc/ssh/sshd_config.d/00-test.conf"
w "mkdir /etc/systemd/system/firewall.service.d"
w "write tests/test-ssh.conf /etc/systemd/system/firewall.service.d/test-ssh.conf"
for f in /etc/ssh/sshd_config.d/00-test.conf /etc/systemd/system/firewall.service.d/test-ssh.conf; do
for x in "mode 0100644" "uid 0" "gid 0"; do w "set_inode_field $f $x"; done
done
for x in "mode 040755" "uid 0" "gid 0"; do w "set_inode_field /etc/systemd/system/firewall.service.d $x"; done
fusermount3 -u "$W/fuse.raw"
pkill -f "^qemu-storage-daemon .*$W/check.qcow2" || true
sleep 1
EXTRA=()
for a in "$@"; do
case "$a" in
--wrong-clock) EXTRA+=(-rtc "base=$(date -u -d '-3 hours' +%Y-%m-%dT%H:%M:%S)") ;;
--usb) EXTRA+=(-device qemu-xhci,id=xhci) ;;
esac
done
setsid qemu-system-x86_64 -name acpc2026-check -machine q35,accel=kvm -cpu host -m 6144 -smp 4 \
-drive if=pflash,format=raw,readonly=on,file="$CODE" \
-drive if=pflash,format=raw,file="$W/vars.fd" \
-drive id=disk0,file="$W/check.qcow2",format=qcow2,if=none \
-device virtio-blk-pci,drive=disk0,bootindex=1 \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \
-vga virtio -display none -monitor unix:"$MON",server,nowait \
-serial file:"$W/serial.log" "${EXTRA[@]}" > "$W/qemu.log" 2>&1 &
for _ in $(seq 1 120); do
tests/vssh.sh team true 2>/dev/null && break
sleep 3
done
# Wait for the desktop session of the team account
for _ in $(seq 1 60); do
tests/vssh.sh team 'loginctl list-sessions --no-legend' 2>/dev/null | grep -q seat0 && break
sleep 2
done
echo "Test VM ready (SSH 127.0.0.1:2222, monitor $MON)."
;;
stop)
if running; then
python3 -I tests/monitor.py "$MON" quit || true
for _ in $(seq 1 30); do running || break; sleep 1; done
fi
rm -rf "$W"
echo "Test VM stopped."
;;
*)
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
exit 2
;;
esac
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Mouse on the team desktop (needs tests/add_test_tools.sh), screen pixels:
# tests/click.sh X Y left click
# tests/click.sh X Y right right click
# tests/click.sh X Y double double click
case "${3:-left}" in
right) action="click 3" ;;
double) action="click --repeat 2 --delay 120 1" ;;
*) action="click 1" ;;
esac
exec "$(dirname "$0")/vssh.sh" team "DISPLAY=:0 XAUTHORITY=\$HOME/.Xauthority xdotool mousemove $1 $2 sleep 0.2 $action"
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# Runs the checklist of every playbook feature in the test VM, as root:
# ADMIN_PASS=... tests/feature_check.sh -> test_report/feature_check.txt
cd "$(dirname "$0")/.."
mkdir -p test_report
sshpass -p "${ADMIN_PASS:?set ADMIN_PASS}" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -P 2222 tests/feature_check_vm.sh icpc@127.0.0.1:/tmp/feature_check_vm.sh
tests/vssh.sh icpc "echo '$ADMIN_PASS' | sudo -S -p '' bash /tmp/feature_check_vm.sh" | tee test_report/feature_check.txt
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# Runs inside the VM as root. Checks every feature of the Ansible playbooks.
pass=0; fail=0
c() { # c "description" command...
local d=$1; shift
if eval "$@" >/dev/null 2>&1; then echo "PASS $d"; pass=$((pass+1)); else echo "FAIL $d"; fail=$((fail+1)); fi
}
as_team() { runuser -u team -- bash -c "$1"; }
section() { echo; echo "== $1"; }
section "accounts.yml / finish.yml"
c "team is logged in automatically on the screen" "loginctl list-sessions --no-legend | grep -q 'team seat0'"
c "team is not in the sudo group" "! id -nG team | grep -qw sudo"
c "team is not in the lpadmin group" "! id -nG team | grep -qw lpadmin"
c "icpc is in the sudo group" "id -nG icpc | grep -qw sudo"
c "root password is locked" "passwd -S root | grep -q ' L '"
c "sudo rule of vscode-extension-install removed" "[ ! -e /etc/sudoers.d/vscode-extension-install ]"
c "vscode-extension-install not started at login" "grep -q '^Hidden=true' /etc/xdg/autostart/vscode-extension-install.desktop"
c "VS Code extensions mounted for team at boot" "systemctl is-active -q team-vscode-extensions && findmnt /home/team/.vscode/extensions"
c "boot menu: superuser and password" "grep -q 'set superusers=\"admin\"' /boot/grub/grub.cfg && grep -q password_pbkdf2 /boot/grub/grub.cfg"
c "boot menu: normal entries unrestricted" "grep -q -- '--unrestricted' /boot/grub/grub.cfg"
c "boot menu: no recovery entries" "! grep -q recovery /boot/grub/grub.cfg"
c "boot menu password file is root only" "[ \$(stat -c %a /etc/grub.d/01_acpc_password) = 700 ]"
c "SSH settings of the build removed" "[ ! -e /etc/ssh/sshd_config.d/00-build.conf ]"
section "languages.yml"
c "gcc-15 15.2.0" "[ \$(gcc-15 -dumpfullversion) = 15.2.0 ]"
c "g++-15 15.2.0" "[ \$(g++-15 -dumpfullversion) = 15.2.0 ]"
c "java 25.0.4.1" "java -version 2>&1 | grep -q '\"25.0.4.1\"'"
c "javac 25.0.4.1" "[ \"\$(javac -version 2>&1)\" = 'javac 25.0.4.1' ]"
c "PyPy 7.3.23 / Python 3.11.15" "pypy3 -c 'import sys; assert sys.pypy_version_info[:3]==(7,3,23) and sys.version_info[:3]==(3,11,15)'"
c "Java packages held at 25.0.4.1" "apt-mark showhold | grep -q openjdk-25-jdk-headless"
c "compilec uses the Kattis command" "grep -q 'gcc-15 -g -O2 -o a.out -std=gnu23 -static \"\$@\" -lm' /usr/local/bin/compilec"
c "compilecpp uses the Kattis command" "grep -q 'g++-15 -g -O2 -o a.out -std=gnu++23 -static -lrt -Wl,--whole-archive -lpthread -Wl,--no-whole-archive' /usr/local/bin/compilecpp"
c "compilejava uses the Kattis command" "grep -q \"javac -encoding UTF-8 -sourcepath . -cp './\\*' -d .\" /usr/local/bin/compilejava"
c "runjava uses the Kattis options" "grep -q 'UseSerialGC -Xss64m -XX:TieredStopAtLevel=1 -XX:ActiveProcessorCount=2' /usr/local/bin/runjava"
c "runpython3 uses pypy3" "grep -q 'exec pypy3' /usr/local/bin/runpython3"
c "compilegcc and compileg++ are other names" "[ \$(readlink /usr/local/bin/compilegcc) = /usr/local/bin/compilec ] && [ \$(readlink /usr/local/bin/compileg++) = /usr/local/bin/compilecpp ]"
c "technical notes are the ACPC 2026 ones" "pdftotext /usr/share/doc/icpc/TechNotes.pdf - | grep -q 'Atlantic Canadian Programming Competition 2026'"
section "firefox.yml"
P=/usr/lib/firefox/distribution/policies.json
c "home page is the contest" "python3 -c \"import json;p=json.load(open('$P'))['policies'];assert p['Homepage']['URL']=='https://atlantic-canada26.kattis.com' and p['Homepage']['Locked']\""
c "contest, offline help and documentation bookmarks" "python3 -c \"import json;p=json.load(open('$P'))['policies'];assert [b['Title'] for b in p['Bookmarks']]==['Contest','Problems','Standings','Contest Help (offline)','Language Documentation','Technical Notes'];assert p['Bookmarks'][3]['URL']=='file:///usr/share/doc/acpc/help/index.html'\""
c "encrypted client hello off (locked)" "python3 -c \"import json;p=json.load(open('$P'))['policies']['Preferences'];assert p['network.dns.echconfig.enabled']=={'Value':False,'Status':'locked'}\""
c "extension installs blocked" "python3 -c \"import json;p=json.load(open('$P'))['policies'];assert p['ExtensionSettings']['*']['installation_mode']=='blocked'\""
c "about:config blocked" "python3 -c \"import json;p=json.load(open('$P'))['policies'];assert p['BlockAboutConfig'] is True\""
section "desktop.yml"
c "World Finals menu entries removed" "! ls /usr/share/applications/{ccs,scoreboard,problems,ccsdocs,judgingnotes}.desktop"
c "Contest menu: Contest, Problems, Standings and offline help" "ls /usr/share/applications/acpc-{contest,problems,standings,help}.desktop"
c "Documentation menu entry named Language Documentation" "grep -q '^Name=Language Documentation' /usr/share/applications/devdocs.desktop"
c "submit points to Kattis" "as_team 'submit' | grep -q 'This contest runs on Kattis'"
c "pc2submit points to Kattis" "as_team 'pc2submit' | grep -q 'This contest runs on Kattis'"
c "Kotlin compile/run commands removed" "[ ! -e /usr/local/bin/compilekotlin ] && [ ! -e /usr/local/bin/runkotlin ]"
c "JetBrains agreement answered for team" "grep -rq eua_accepted_version /home/team/.java/.userPrefs/jetbrains/"
c "JetBrains data sharing declined for team" "grep -q 'rsch.send.usage.stat:1.1:0' /home/team/.local/share/JetBrains/consentOptions/accepted"
c "same answers in /etc/skel" "grep -rq eua_accepted_version /etc/skel/.java/.userPrefs/jetbrains/ && [ -f /etc/skel/.local/share/JetBrains/consentOptions/accepted ]"
c "PyCharm: PyPy declared and used by default" "grep -q '/usr/local/bin/pypy3' /home/team/.config/JetBrains/PyCharm*/options/jdk.table.xml && grep -q 'PyPy 3.11' /home/team/.config/JetBrains/PyCharm*/options/project.default.xml"
c "CLion, IntelliJ IDEA, PyCharm trust /home/team" "[ \$(grep -l '/home/team' /home/team/.config/JetBrains/*/options/trusted-paths.xml | wc -l) = 3 ]"
c "same JetBrains settings in /etc/skel" "[ \$(ls /etc/skel/.config/JetBrains/*/options/trusted-paths.xml | wc -l) = 3 ]"
c "Eclipse runs on Java 25.0.4.1" "grep -qx '/usr/lib/jvm/java-25-openjdk-amd64/bin/java' /opt/eclipse/eclipse.ini"
c "Geany C build uses the contest compiler driver" "grep -q '^FT_01_CM=acpc-gcc' /usr/share/geany/filedefs/filetypes.c"
c "Geany C++ build uses the contest compiler driver" "grep -q '^FT_01_CM=acpc-g++' /usr/share/geany/filedefs/filetypes.cpp"
c "Geany runs Python with PyPy" "grep -q '^EX_00_CM=pypy3' /usr/share/geany/filedefs/filetypes.python"
c "VS Code uses PyPy for Python" "grep -q '/usr/local/bin/pypy3' /home/team/.config/Code/User/settings.json"
c "Geany Java uses contest compilation and VM options" "grep -q '^FT_01_CM=compilejava' /usr/share/geany/filedefs/filetypes.java && grep -q '^EX_00_CM=runjava' /usr/share/geany/filedefs/filetypes.java"
c "CLion C and C++ toolchain drivers" "grep -q 'customCCompilerPath' /home/team/.config/JetBrains/CLion*/options/linux/toolchains.xml && grep -q 'customCXXCompilerPath' /home/team/.config/JetBrains/CLion*/options/linux/toolchains.xml"
c "CLion Python defaults to PyPy" "grep -q /usr/local/bin/pypy3 /home/team/.config/JetBrains/CLion*/options/jdk.table.xml"
c "IntelliJ Java SDK and VM options" "grep -q java-25-openjdk /home/team/.config/JetBrains/IntelliJIdea*/options/jdk.table.xml && grep -q UseSerialGC /home/team/.config/JetBrains/IntelliJIdea*/options/project.default.xml"
c "CodeBlocks C and C++ compiler profile" "grep -q acpc-gcc /home/team/.config/codeblocks/default.conf && grep -q acpc-g++ /home/team/.config/codeblocks/default.conf"
c "VS Code contest build actions and F6 shortcut" "grep -q acpc-solution /home/team/.config/Code/User/tasks.json && grep -q f6 /home/team/.config/Code/User/keybindings.json"
c "Eclipse menu initializes the contest workspace" "grep -q '^Exec=/usr/local/bin/acpc-eclipse' /usr/share/applications/Eclipse.desktop"
c "Eclipse PyDev uses PyPy without changing Java/CDT build preferences" "grep -q /usr/local/bin/pypy3 /home/team/eclipse-workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.python.pydev.prefs && grep -qx CHECK_PREFERRED_PYDEV_SETTINGS=false /home/team/eclipse-workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.python.pydev.prefs"
c "IDE profiles restored by reset from skel" "test -f /etc/skel/.config/codeblocks/default.conf && test -f /etc/skel/.config/Code/User/tasks.json && test -f /etc/skel/eclipse-workspace/.metadata/.plugins/org.eclipse.debug.core/.launches/ACPC\ Build\ and\ run.launch"
section "docs.yml"
c "offline help is installed, root-owned and readable by team" "[ \"\$(stat -c '%U:%G:%a' /usr/share/doc/acpc/help/index.html)\" = root:root:644 ] && as_team 'test -r /usr/share/doc/acpc/help/index.html'"
c "offline help menu opens the local document" "grep -qx 'Exec=firefox file:///usr/share/doc/acpc/help/index.html' /usr/share/applications/acpc-help.desktop"
c "DevDocs runs" "systemctl is-active -q container-devdocs && curl -sk -o /dev/null https://devdocs/"
c "DevDocs has exactly C, C++, OpenJDK 25, Python 3.11" "[ \"\$(podman exec devdocs ls public/docs | tr '\n' ' ')\" = 'c cpp docs.json openjdk~25 python~3.11 ' ]"
c "Kotlin gone from the compiled list" "! podman exec devdocs sh -c 'grep -q kotlin public/assets/docs-*.js'"
c "no 'must be loaded from devdocs.io' notice" "podman exec devdocs sh -c 'grep -q production_host:\\\"devdocs\\\" public/assets/application-*.js'"
section "firewall.yml"
c "firewall, proxy and DNS filter running" "systemctl is-active -q firewall contest-proxy contest-dns"
c "test_firewall.sh: 15 OK, no problem (as team)" "[ \$(as_team /test_firewall.sh | grep -c '^OK') = 15 ]"
c "web egress only for the proxy account" "nft list chain inet filter output | grep -q \"skuid \$(id -u contest-proxy) tcp dport 443 accept\""
c "DNS egress only for the proxy account" "nft list chain inet filter output | grep -q \"skuid \$(id -u contest-proxy) ip daddr 1.1.1.1 udp dport 53\""
c "NTP egress only for chrony" "nft list chain inet filter output | grep -q \"skuid \$(id -u _chrony) ip daddr @ntp_ips udp dport 123 accept\""
c "incoming connections dropped (policy drop)" "nft list chain inet filter input | grep -q 'policy drop'"
c "resolv.conf points to the DNS filter" "[ \"\$(cat /etc/resolv.conf)\" = 'nameserver 127.0.0.1' ]"
c "open.kattis.com in /etc/hosts" "grep -q 'open.kattis.com' /etc/hosts"
c "clock synchronized by chrony" "chronyc -n sources | grep -q '^\\^\\*'"
c "chrony does not write the hardware clock" "! grep -q '^rtcsync' /etc/chrony/chrony.conf"
c "Ubuntu NTS sources disabled" "[ ! -e /etc/chrony/sources.d/ubuntu-ntp-pools.sources ]"
c "clock fallback timer enabled" "systemctl is-enabled -q time_from_contest.timer"
c "time zone America/Moncton" "[ \$(readlink -f /etc/localtime) = /usr/share/zoneinfo/America/Moncton ]"
section "lockdown.yml"
c "no local AI models in the JetBrains IDEs" "[ -z \"\$(find /opt -name 'full-line-model-*' -o -name 'full-line-native-server-*')\" ]"
c "USBGuard running, rejects new devices" "systemctl is-active -q usbguard && grep -q '^ImplicitPolicyTarget=reject' /etc/usbguard/usbguard-daemon.conf && grep -q '^PresentDevicePolicy=allow' /etc/usbguard/usbguard-daemon.conf"
c "polkit: removable media refused to teams" "[ -f /etc/polkit-1/rules.d/10-acpc-no-removable-media.rules ]"
c "polkit: network changes refused to teams" "[ -f /etc/polkit-1/rules.d/10-acpc-no-network-changes.rules ]"
c "polkit: restart/power off refused to teams" "[ -f /etc/polkit-1/rules.d/10-acpc-no-power-actions.rules ]"
c "udev: no direct access to phones" "[ -f /etc/udev/rules.d/72-acpc-no-phone-access.rules ]"
c "team cannot reboot" "! systemd-run --machine=team@ --user --wait --pipe --quiet systemctl reboot"
c "team cannot change the network" "systemd-run --machine=team@ --user --wait --pipe --quiet nmcli general permissions | grep -q 'network-control *no'"
c "Ctrl+Alt+Del masked" "[ \$(systemctl is-enabled ctrl-alt-del.target 2>&1) = masked ]"
c "SysRq off" "[ \$(cat /proc/sys/kernel/sysrq) = 0 ]"
c "power/reboot keys ignored" "grep -q '^HandlePowerKey=ignore' /etc/systemd/logind.conf.d/90-acpc-keys.conf"
c "desktop waits for the clock" "systemctl is-enabled -q wait-for-time"
c "automatic updates masked" "[ \"\$(systemctl is-enabled apt-daily.timer apt-daily-upgrade.timer unattended-upgrades 2>&1 | sort -u)\" = masked ]"
c "contest-restrictions installed, root only" "[ \$(stat -c %a /usr/local/sbin/contest-restrictions) = 700 ]"
c "contest-restrictions status: all on" "[ \$(contest-restrictions status | grep -c ' on\$') = 3 ]"
c "banner service running (root)" "systemctl is-active -q contest-restrictions-warning"
c "/etc/contest-restrictions root only" "[ \$(stat -c %a /etc/contest-restrictions) = 700 ]"
c "admin scripts unreadable by team" "! as_team 'cat /disable_firewall.sh /usr/local/sbin/contest-restrictions /reset_team_home.sh'"
c "reset script installed" "[ -x /reset_team_home.sh ]"
echo
echo "$pass passed, $fail failed"
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Starts a program on the desktop of the team account: tests/gui.sh 'firefox https://devdocs/'
exec "$(dirname "$0")/vssh.sh" team "export DISPLAY=:0 XAUTHORITY=\$HOME/.Xauthority; (setsid $1 >/dev/null 2>&1 &)"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p test_report/ide-config
tar -C tests -cf - ide_check_vm.py ide_samples | tests/vssh.sh team \
'mkdir -p /tmp/acpc-ide-check && tar -xf - -C /tmp/acpc-ide-check'
tests/vssh.sh team 'python3 /tmp/acpc-ide-check/ide_check_vm.py' | tee test_report/ide-config/integration-check.txt
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/python3
"""Integration checks in the built image; graphical checks are recorded separately."""
import configparser
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET
passed = failed = 0
def check(name, fn):
global passed, failed
try:
fn()
print('PASS ', name, flush=True)
passed += 1
except Exception as error:
print('FAIL ', name, ':', error, flush=True)
failed += 1
def require(condition, detail='assertion failed'):
if not condition:
raise AssertionError(detail)
def run(args, **kwargs):
result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=45, **kwargs)
require(result.returncode == 0, result.stdout)
return result.stdout
samples = Path(__file__).parent / 'ide_samples'
with tempfile.TemporaryDirectory(prefix='ACPC IDE check ') as directory:
work = Path(directory)
for filename in ('sum.c', 'sum.cpp', 'Sum.java', 'sum.py'):
case = work / filename.replace('.', '-')
case.mkdir()
source = case / filename
shutil.copy(samples / filename, source)
def exercise(source=source):
output = run(['acpc-solution', 'run', str(source)], input='3000000000 4\n')
require('SUM = 3000000004' in output, output)
if source.suffix in ('.c', '.cpp'):
require('statically linked' in run(['file', str(source.with_suffix(''))]))
if source.suffix == '.java':
require(int.from_bytes(source.with_suffix('.class').read_bytes()[6:8], 'big') == 69)
print(' ' + output.strip().replace('\n', '\n '))
check('build and execute ' + filename + ' (path contains spaces)', exercise)
def driver_flags():
log = work / 'compiler.jsonl'
for lang, driver, old_standard, expected, library in (
('c', 'acpc-gcc', 'c99', 'gnu23', '-lm'),
('cpp', 'acpc-g++', 'c++17', 'gnu++23', '-lpthread'),
):
source = work / ('sum-' + lang) / ('sum.' + lang)
run([driver, '-O0', '-std=' + old_standard, str(source), '-o', str(work / driver)],
env={**os.environ, 'ACPC_COMPILER_LOG': str(log)})
args = json.loads(log.read_text().splitlines()[-1])['argv']
require('-O0' not in args and '-O2' in args and '-std=' + expected in args, str(args))
require('-static' in args and library in args, str(args))
require('15.2.0' in run(['acpc-gcc', '-dumpfullversion']))
require('15.2.0' in run(['acpc-g++', '--version']))
output = run(['acpc-cc', '-x', 'c++', '-dM', '-E', '-'], input='')
require('#define __cplusplus 202302L' in output, 'C++ compiler probes must use the C++ backend')
response = work / 'compiler probe.args'
response.write_text('-x c++ -std=c++17 -dM -E -')
output = run(['acpc-cc', '@' + str(response)], input='')
require('#define __cplusplus 202302L' in output,
'CLion response-file probes must see the same C++23 standard as compilation')
check('compiler drivers override generic project flags, link statically and answer IDE probes', driver_flags)
def failed_build():
case = work / 'sum-cpp'
source = case / 'sum.cpp'
source.write_text('#error deliberate bad source\n')
result = subprocess.run(['acpc-solution', 'run', str(source)], text=True, input='1 2\n', capture_output=True)
require(result.returncode != 0 and 'SUM =' not in result.stdout and 'Run:' not in result.stdout,
'a failed build must never execute the previous binary')
check('failed build does not execute a stale binary', failed_build)
def geany_commands():
for language, filename in [('c', 'sum.c'), ('cpp', 'sum.cpp'), ('java', 'Sum.java'), ('python', 'sum.py')]:
conf = configparser.ConfigParser(interpolation=None, strict=False)
conf.read('/usr/share/geany/filedefs/filetypes.' + language)
source = work / ('geany ' + language) / filename
source.parent.mkdir()
shutil.copy(samples / filename, source)
for key in ('FT_00_CM', 'FT_01_CM', 'EX_00_CM'):
command = conf['build-menu'][key].replace('%f', source.name).replace('%e', source.stem)
result = run(['bash', '-c', command], cwd=source.parent, input='3000000000 4\n')
if key == 'EX_00_CM': require('SUM = 3000000004' in result, result)
if key == 'FT_00_CM' and source.suffix in ('.c', '.cpp'):
require('relocatable' in run(['file', str(source.with_suffix('.o'))]))
check('all four installed Geany build and execute commands work', geany_commands)
for home in (Path('/home/team'), Path('/etc/skel')):
def profiles(home=home):
cb = ET.parse(home / '.config/codeblocks/default.conf')
cb_root = Path(cb.findtext('./compiler/sets/gcc/MASTER_PATH/str')) / 'bin'
require((cb_root / cb.findtext('./compiler/sets/gcc/C_COMPILER/str')).resolve() == Path('/usr/local/bin/acpc-cc'))
require((cb_root / cb.findtext('./compiler/sets/gcc/CPP_COMPILER/str')).resolve() == Path('/usr/local/bin/acpc-cc'))
require(not Path(cb.findtext('./compiler/sets/gcc/C_COMPILER/str')).is_absolute(),
'Code::Blocks prefixes MASTER_PATH/bin, including for an absolute filename')
code = home / '.config/Code/User'
tasks = json.loads((code / 'tasks.json').read_text())['tasks']
require(any(t['label'] == 'ACPC: Build and run' and t['command'] == '/usr/local/bin/acpc-solution' for t in tasks))
require(any(b.get('key') == 'f6' and b.get('args') == 'ACPC: Build and run' for b in json.loads((code / 'keybindings.json').read_text())))
for product in ('CLion', 'PyCharm'):
options = next((home / '.config/JetBrains').glob(product + '*/options'))
require('/usr/local/bin/pypy3' in (options / 'jdk.table.xml').read_text())
options = next((home / '.config/JetBrains').glob('IntelliJIdea*/options'))
require('ACPC Java 25' in (options / 'jdk.table.xml').read_text())
require('-XX:+UseSerialGC' in (options / 'project.default.xml').read_text())
options = next((home / '.config/JetBrains').glob('CLion*/options'))
chain = ET.parse(options / 'linux/toolchains.xml').find('./component/toolchains/toolchain')
require(chain.get('customCCompilerPath') == '/usr/local/bin/acpc-gcc')
require(chain.get('customCXXCompilerPath') == '/usr/local/bin/acpc-g++')
workspace = home / 'eclipse-workspace/.metadata/.plugins'
launch = ET.parse(workspace / 'org.eclipse.debug.core/.launches/ACPC Build and run.launch')
require(any(n.get('value') == '/usr/local/bin/acpc-solution' for n in launch.getroot()))
prefs = workspace / 'org.eclipse.core.runtime/.settings'
require('/usr/local/bin/pypy3' in (prefs / 'org.python.pydev.prefs').read_text())
require('java-25-openjdk' in (prefs / 'org.eclipse.jdt.launching.prefs').read_text())
check(str(home) + ': settings for every IDE are present and parseable', profiles)
print(f'\n{passed} passed, {failed} failed')
raise SystemExit(bool(failed))
+16
View File
@@ -0,0 +1,16 @@
import java.lang.management.ManagementFactory;
import java.util.Scanner;
public class Sum {
public static void main(String[] args) {
if (!System.getProperty("java.version").equals("25.0.4.1"))
throw new IllegalStateException("Java 25.0.4.1 is required");
var vmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
for (String flag : new String[]{"-Dfile.encoding=UTF-8", "-XX:+UseSerialGC", "-Xss64m", "-XX:TieredStopAtLevel=1", "-XX:ActiveProcessorCount=2", "-Xms2048m", "-Xmx2048m"})
if (!vmArgs.contains(flag)) throw new IllegalStateException("Missing " + flag);
System.out.println("Java " + System.getProperty("java.version"));
System.out.println("VM: " + vmArgs);
System.out.print("Input two integers: ");
Scanner in = new Scanner(System.in);
System.out.println("SUM = " + (in.nextLong() + in.nextLong()));
}
}
+15
View File
@@ -0,0 +1,15 @@
#include <stdio.h>
#ifdef __cplusplus
#error This C file must be compiled as C, not C++.
#endif
#if __STDC_VERSION__ < 202311L
#error C23 is required.
#endif
int main(void) {
long long a, b;
printf("C23 | GCC %s\nInput two integers: ", __VERSION__);
fflush(stdout);
if (scanf("%lld %lld", &a, &b) != 2) return 1;
printf("SUM = %lld\n", a + b);
return 0;
}
+12
View File
@@ -0,0 +1,12 @@
#include <iostream>
#include <print>
#if __cplusplus < 202302L
#error C++23 is required.
#endif
int main() {
std::println("C++23 | GCC {}", __VERSION__);
std::cout << "Input two integers: " << std::flush;
long long a, b;
if (!(std::cin >> a >> b)) return 1;
std::println("SUM = {}", a + b);
}
+8
View File
@@ -0,0 +1,8 @@
import platform
import sys
assert platform.python_implementation() == 'PyPy'
assert sys.pypy_version_info[:3] == (7, 3, 23)
assert sys.version_info[:3] == (3, 11, 15)
print('PyPy 7.3.23 | Python ' + platform.python_version())
values = input('Input two integers: ').split()
print('SUM =', sum(map(int, values)))
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Types on the keyboard of the test VM: tests/key.sh 'text{{ret}}' or tests/key.sh @file
cd "$(dirname "$0")/.."
exec python3 -I tests/type.py tests/work/monitor.sock "$1"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/python3
"""Create a portable HTML report from reviewed screenshots and recorded results."""
import base64
from datetime import datetime, timezone
import html
import json
from pathlib import Path
root = Path(__file__).resolve().parent.parent
report = root / 'test_report/ide-config'
data = json.loads((report / 'results.json').read_text())
e = html.escape
def photo(name, caption):
content = base64.b64encode((report / name).read_bytes()).decode()
url = 'data:image/png;base64,' + content
return f'<figure><button type="button" class="enlarge" aria-label="Enlarge {e(caption)}"><img src="{url}" loading="lazy" alt="{e(caption)}"></button><figcaption>{e(caption)} · <a href="{url}" download="{e(name)}">PNG</a></figcaption></figure>'
cases = data['cases']
total = len(cases)
passed = sum(c['status'] == 'pass' for c in cases)
rows = []
cards = []
for case in cases:
ident = case['id']
rows.append(f'<tr><td><a href="#{e(ident)}">{e(case["ide"])}</a></td><td>{e(case["language"])}</td><td class="{e(case["status"])}">{e(case["status"].upper())}</td><td>{e(case["action"])}</td></tr>')
pictures = ''.join(photo(p['file'], p['caption']) for p in case['screenshots'])
cards.append(f'<article id="{e(ident)}" data-ide="{e(case["ide"])}"><h2>{e(case["ide"])} · {e(case["language"])} <span class="badge {e(case["status"])}">{e(case["status"].upper())}</span></h2><p><strong>Action:</strong> {e(case["action"])}</p><p>{e(case["evidence"])}</p>{pictures}</article>')
logs = []
for name, title in [('integration-check.txt', 'Compiler and configuration integration checks'),
('feature-check.txt', 'Image feature checks'), ('technotes-check.txt', 'Technical notes checks'),
('idempotence.txt', 'Configuration reapplied without changes'),
('reset-check.txt', 'Team-home reset and restored configuration'),
('reset-integration-check.txt', 'Compiler integration checks after the team-home reset'),
('build-summary.txt', 'Final image build and installed file checksums'),
('gui-artifacts.txt', 'Artifacts created by graphical runs')]:
path = report / name
if path.exists():
logs.append(f'<details><summary>{e(title)}</summary><pre>{e(path.read_text())}</pre></details>')
sources = ''.join(f'<li><a href="{e(s["url"])}">{e(s["title"])}</a></li>' for s in data['sources'])
fixtures = ''.join(f'<details><summary>{e(p.name)} — test source</summary><pre>{e(p.read_text())}</pre></details>' for p in sorted((root / 'tests/ide_samples').iterdir()) if p.is_file())
options = ''.join(f'<option>{e(ide)}</option>' for ide in dict.fromkeys(c['ide'] for c in cases))
notes = ''.join(f'<li>{e(note)}</li>' for note in data['notes'])
build = e(data['build'])
notes_page = report / 'technotes-final-3.png'
instructions = ('<section class="panel"><h2>Instructions supplied to teams</h2><p>Page 3 of the updated TechNotes explains the tested action for every IDE and the first-launch dialogs. The PDF installed in the final image was checked against this copy.</p>' + photo(notes_page.name, 'TechNotes · IDE instructions, page 3') + '</section>') if notes_page.exists() else ''
page = f'''<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>ACPC 2026 — IDE compiler verification</title>
<style>
:root{{font-family:system-ui,sans-serif;color:#172434;background:#f1f4f7;line-height:1.55}}body{{margin:0}}main{{max-width:1100px;margin:auto;padding:32px 22px 70px}}header,article,.panel{{background:white;border:1px solid #d7e0e8;border-radius:12px;padding:24px;margin:0 0 24px}}h1{{font-size:2rem;line-height:1.2;margin:.2em 0 .6em}}h2{{font-size:1.35rem}}.eyebrow{{color:#526579;font-weight:650;letter-spacing:.07em;text-transform:uppercase;font-size:.8rem}}a{{color:#155b96}}table{{width:100%;border-collapse:collapse}}th,td{{text-align:left;border-bottom:1px solid #dce4ec;padding:9px 8px}}th{{background:#edf3f8}}.pass{{color:#11603d}}.fail{{color:#a12121}}.pending{{color:#835800}}.badge{{border:1px solid currentColor;border-radius:5px;padding:2px 7px;font-size:.75rem;vertical-align:middle;margin-left:8px}}img{{max-width:100%;height:auto;display:block;border:1px solid #ced9e3;border-radius:6px}}figure{{margin:20px 0 0}}figcaption{{font-size:.9rem;margin:7px 0;color:#42576a}}pre{{white-space:pre-wrap;overflow-wrap:anywhere;background:#edf2f6;padding:16px;border-radius:6px;font-size:.85rem}}summary{{cursor:pointer;font-weight:600;padding:10px 0}}select{{font:inherit;padding:6px 12px;margin-left:10px}}.sub{{color:#516378}}.grid{{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}}.metric{{background:#edf6f0;padding:13px;border-radius:7px}}.metric b{{display:block;font-size:1.6rem}}article{{scroll-margin-top:20px}}.tablewrap{{overflow-x:auto}}@media(max-width:650px){{.grid{{grid-template-columns:1fr}}main{{padding:15px 10px}}header,article,.panel{{padding:16px}}h1{{font-size:1.6rem}}}}@media print{{details{{display:block}}select{{display:none}}article{{break-inside:avoid}}body{{background:white}}}}
</style><main>
<style>.enlarge{{display:block;padding:0;border:0;background:none;cursor:zoom-in;width:100%;text-align:left}}dialog{{max-width:96vw;max-height:94vh;padding:14px;border:1px solid #98aabd;border-radius:9px}}dialog::backdrop{{background:#102333cc}}dialog img{{max-width:none;max-height:none}}dialog form{{position:sticky;top:0;text-align:right;margin-bottom:10px;background:white}}dialog button{{font:inherit;padding:5px 15px}}</style>
<header><div class="eyebrow">ACPC 2026 · contest USB image</div><h1>IDE compiler verification</h1>
<p>{e(data['verdict'])}</p><div class="grid"><div class="metric"><b>{passed}/{total}</b>language workflows passed</div><div class="metric"><b>7</b>IDEs on the real VM desktop</div><div class="metric"><b>3000000004</b>expected result for 3000000000 + 4</div></div>
<p class="sub">{build}</p><p class="sub">Screenshots are embedded in this file. Click any image to inspect it, or download its PNG. No external service is needed to view this report.</p></header>
<section class="panel"><h2>What was tested</h2><p>Each recorded workflow was launched through the IDE interface, built or interpreted the supplied source, accepted input typed on the VM keyboard and produced the expected result. The screenshots were opened and visually inspected. Configuration was installed by Ansible; compiler settings were not repaired manually during the final GUI pass.</p><p>C rejects accidental C++ compilation and requires C23. C++ requires C++23 and uses <code>std::println</code>. Java asserts the exact runtime version and the required VM flags. Python asserts PyPy and its version. Supplementary checks inspect static C/C++ executables and Java class versions.</p><ul>{notes}</ul></section>
<section class="panel"><h2>Results</h2><div class="tablewrap"><table><thead><tr><th>IDE</th><th>Language</th><th>Result</th><th>Action tested</th></tr></thead><tbody>{''.join(rows)}</tbody></table></div></section>
<p><label>Show screenshots for <select id="filter"><option value="all">All IDEs</option>{options}</select></label></p>
{''.join(cards)}
{instructions}
<section class="panel"><h2>Supporting checks</h2>{''.join(logs)}{fixtures}</section>
<section class="panel"><h2>Configuration references</h2><p>The installed IDE files and their serialized settings were also inspected. These primary references explain the supported configuration mechanisms.</p><ul>{sources}</ul><p class="sub">Report generated {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}.</p></section>
</main><dialog id="viewer"><form method="dialog"><button>Close</button></form><img alt=""></dialog><script>const filter=document.getElementById('filter');function applyFilter(){{document.querySelectorAll('article[data-ide]').forEach(a=>a.hidden=filter.value!=='all'&&a.dataset.ide!==filter.value)}}filter.addEventListener('change',applyFilter);window.addEventListener('pageshow',applyFilter);document.querySelectorAll('a[href^="#"]').forEach(a=>a.addEventListener('click',()=>{{document.getElementById('filter').value='all';document.querySelectorAll('article[data-ide]').forEach(a=>a.hidden=false)}}));const viewer=document.getElementById('viewer');document.querySelectorAll('.enlarge').forEach(button=>button.addEventListener('click',()=>{{const source=button.querySelector('img'),target=viewer.querySelector('img');target.src=source.src;target.alt=source.alt;viewer.showModal()}}));viewer.addEventListener('click',event=>{{if(event.target===viewer)viewer.close()}});</script></html>'''
(report / 'index.html').write_text(page)
print(f'{report / "index.html"}: {passed}/{total} passed; {len(page):,} bytes')
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# Builds test_report/index.html from the screenshots and test_report/captions.txt
# (one line per screenshot: NAME | caption), plus test_report/feature_check.txt.
import html, os
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'test_report'))
captions = {}
if os.path.exists('captions.txt'):
for line in open('captions.txt'):
if '|' in line:
name, text = line.split('|', 1)
captions[name.strip()] = text.strip()
names = sorted(f[:-4] for f in os.listdir('.') if f.endswith('.png'))
order = [n for n in captions if n in names] + [n for n in names if n not in captions]
out = ['<!DOCTYPE html><html><head><meta charset="utf-8"><title>ACPC 2026 image, test report</title>',
'<style>body{font-family:sans-serif;max-width:1300px;margin:auto;padding:20px}'
'img{max-width:100%;border:1px solid #999}figure{margin:0 0 40px 0}'
'figcaption{font-size:1.1em;margin:6px 0}pre{background:#f4f4f4;padding:12px}</style></head><body>',
'<h1>ACPC 2026 contest image: test report</h1>']
if os.path.exists('intro.txt'):
out.append('<p>%s</p>' % html.escape(open('intro.txt').read().strip()))
if os.path.exists('technotes_check.txt'):
out.append('<h2>Technical notes checklist</h2><pre>%s</pre>' % html.escape(open('technotes_check.txt').read()))
if os.path.exists('feature_check.txt'):
out.append('<h2>Feature checklist</h2><pre>%s</pre>' % html.escape(open('feature_check.txt').read()))
out.append('<h2>Screens</h2>')
for n in order:
out.append('<figure><figcaption><b>%s</b> %s</figcaption><img src="%s.png"></figure>'
% (html.escape(n), html.escape(captions.get(n, '')), n))
out.append('</body></html>')
open('index.html', 'w').write('\n'.join(out))
print('test_report/index.html:', len(order), 'screens')
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
# Sends one command to the QEMU monitor: monitor.py SOCKET 'command'
# (screendump FILE, sendkey KEY, device_add ..., quit, ...)
import socket, sys, time
s = socket.socket(socket.AF_UNIX)
s.connect(sys.argv[1])
time.sleep(0.3)
s.recv(65536)
s.sendall((sys.argv[2] + "\n").encode())
time.sleep(0.5)
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# Screenshot of the test VM: tests/shot.sh NAME -> test_report/NAME.png
cd "$(dirname "$0")/.."
mkdir -p test_report tests/work
python3 -I tests/monitor.py tests/work/monitor.sock "screendump $PWD/tests/work/shot.ppm"
sleep 1
python3 -I -c "from PIL import Image; Image.open('tests/work/shot.ppm').save('test_report/$1.png')"
echo "test_report/$1.png"
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# Checks, from the team account in the test VM, the statements of the
# technical notes for the teams (docs/TechNotes.tex) that can be checked on
# the machine: tests/technotes_check.sh -> test_report/technotes_check.txt
cd "$(dirname "$0")/.."
mkdir -p test_report
tests/vssh.sh team 'bash -s' <<'EOF' | tee test_report/technotes_check.txt
set +e
mkdir -p ~/tn && cd ~/tn
pass=0; fail=0
ok(){ echo "PASS $1"; pass=$((pass+1)); }; ko(){ echo "FAIL $1"; fail=$((fail+1)); }
t(){ d=$1; shift; if eval "$@" >/dev/null 2>&1; then ok "$d"; else ko "$d"; fi; }
echo "2 3" > input.txt
cat > base.cpp <<'X'
#include <cstdio>
int main(){int a,b;scanf("%d %d",&a,&b);printf("%d\n",a+b);}
X
for ext in cpp cc C cxx c++; do cp base.cpp "p.$ext"; rm -f a.out
t "C++ extension .$ext: compilecpp, then runcpp < input.txt" "compilecpp p.$ext && [ \"\$(runcpp < input.txt)\" = 5 ]"; done
cat > p.c <<'X'
#include <stdio.h>
int main(void){int a,b;if(scanf("%d %d",&a,&b)!=2)return 1;printf("%d\n",a+b);return 0;}
X
rm -f a.out; t "C: compilec p.c, then runc < input.txt" "compilec p.c && [ \"\$(runc < input.txt)\" = 5 ]"
rm -f a.out; t "compilegcc is another name for compilec" "compilegcc p.c && [ -x a.out ]"
rm -f a.out; t "compileg++ is another name for compilecpp" "compileg++ p.cpp && [ -x a.out ]"
t "programs are linked statically (-static)" "file a.out | grep -q 'statically linked'"
t "compilec runs gcc-15 -g -O2 -o a.out -std=gnu23 -static ... -lm" "grep -q 'gcc-15 -g -O2 -o a.out -std=gnu23 -static' /usr/local/bin/compilec"
t "compilecpp runs the Kattis command" "grep -q 'g++-15 -g -O2 -o a.out -std=gnu++23 -static -lrt -Wl,--whole-archive -lpthread -Wl,--no-whole-archive' /usr/local/bin/compilecpp"
cat > Prog.java <<'X'
import java.util.*;
public class Prog{public static void main(String[] a){Scanner s=new Scanner(System.in);System.out.println(s.nextInt()+s.nextInt());}}
X
t "Java: compilejava Prog.java gives Prog.class, runjava Prog < input.txt" "compilejava Prog.java && [ -f Prog.class ] && [ \"\$(runjava Prog < input.txt)\" = 5 ]"
cat > Mem.java <<'X'
public class Mem{public static void main(String[] a){System.out.println(Runtime.getRuntime().maxMemory()/1048576);}}
X
compilejava Mem.java; m1=$(runjava Mem); m2=$(MEMLIM=1024 runjava Mem)
t "runjava uses 2048 MB (max heap $m1 MB)" "[ $m1 -gt 1900 ] && [ $m1 -le 2048 ]"
t "MEMLIM=1024 runjava uses 1024 MB (max heap $m2 MB)" "[ $m2 -gt 950 ] && [ $m2 -le 1024 ]"
t "java and javac are 25.0.4.1" "java -version 2>&1 | grep -q '\"25.0.4.1\"' && javac -version 2>&1 | grep -q 25.0.4.1"
printf 'a,b=map(int,input().split())\nprint(a+b)\n' > p.py; cp p.py p.py3
t "Python: compilepython3 p.py, then runpython3 p.py < input.txt" "compilepython3 p.py && [ \"\$(runpython3 p.py < input.txt)\" = 5 ]"
t "Python extension .py3 runs" "[ \"\$(runpython3 p.py3 < input.txt)\" = 5 ]"
t "runpython3 runs PyPy 7.3.23 (Python 3.11.15)" "runpython3 -c 'import sys; assert sys.implementation.name==\"pypy\" and sys.pypy_version_info[:3]==(7,3,23) and sys.version_info[:3]==(3,11,15)'"
printf 'print(1/0)\n' > rt.py; t "compilepython3 only checks the syntax (a run time error passes)" "compilepython3 rt.py"
printf 'print(\n' > bad.py; t "compilepython3 reports a syntax error" "! compilepython3 bad.py"
t "python3 is another interpreter, CPython 3.14" "python3 --version | grep -q 'Python 3.14'"
t "gcc-15 and g++-15 are 15.2.0" "[ \$(gcc-15 -dumpfullversion) = 15.2.0 ] && [ \$(g++-15 -dumpfullversion) = 15.2.0 ]"
cat > deep.cpp <<'X'
#include <cstdio>
int f(int n){return n?1+f(n-1):0;}
int main(){printf("%d\n",f(10000000));}
X
t "runcpp: large stack, recursion depth 10 million" "compilecpp deep.cpp && [ \"\$(runcpp)\" = 10000000 ]"
t "man 3 printf works" "man 3 printf | head -5 | grep -qi printf"
t "pypy3 -m pydoc heapq works" "pypy3 -m pydoc heapq | head -3 | grep -q heapq"
t "logged in as team, home /home/team" "[ \"\$(id -un)\" = team ] && [ \"\$HOME\" = /home/team ] && loginctl list-sessions --no-legend | grep -q 'team seat0'"
for app in clion codeblocks Eclipse intellij-idea pycharm com.microsoft.VSCode gvim emacs geany org.kde.kate org.gnome.gedit; do
t "in the Applications menu: $app" "[ -f /usr/share/applications/$app.desktop ]"; done
t "vim and nano are installed" "command -v vim && command -v nano"
t "DevDocs at https://devdocs/ answers" "curl -sk -o /dev/null https://devdocs/"
for d in c cpp openjdk~25 python~3.11; do
t "DevDocs serves the documentation of $d" "[ \$(curl -sk -o /dev/null -w %{http_code} https://devdocs/docs/$d/index.json) = 200 ]"; done
t "DevDocs has no Kotlin documentation" "[ \$(curl -sk -o /dev/null -w %{http_code} https://devdocs/docs/kotlin~2.4/index.json) != 200 ]"
t "offline contest help is readable from the documented menu" "test -r /usr/share/doc/acpc/help/index.html && grep -qx 'Exec=firefox file:///usr/share/doc/acpc/help/index.html' /usr/share/applications/acpc-help.desktop"
t "offline help covers input, judging and every contest language" "python3 -c \"from pathlib import Path;s=Path('/usr/share/doc/acpc/help/index.html').read_text();assert all(x in s for x in ['Reading until EOF','Judgements','C and C++','Java main class','Python 3 and modules'])\""
t "contest site reachable" "curl -s -o /dev/null -m 8 https://atlantic-canada26.kattis.com"
t "Kattis status page reachable" "curl -s -o /dev/null -m 8 https://status.kattis.com"
t "other websites blocked (en.cppreference.com)" "! curl -s -o /dev/null -m 8 https://en.cppreference.com"
t "clock synchronized" "chronyc -n sources | grep -q '^\^\*'"
t "time zone Atlantic (ADT/AST)" "date +%Z | grep -qE 'ADT|AST'"
for source in p.c p.cpp Prog.java p.py; do
t "IDE action acpc-solution builds and runs $source" "acpc-solution run $source < input.txt | tail -1 | grep -qx 5"
done
t "VS Code contest folder and task shortcut exist" "test -d ~/contest && grep -q f6 ~/.config/Code/User/keybindings.json"
t "Eclipse named contest build/run action exists" "test -f ~/eclipse-workspace/.metadata/.plugins/org.eclipse.debug.core/.launches/ACPC\ Build\ and\ run.launch"
t "Geany Java uses runjava for the documented VM options" "grep -q '^EX_00_CM=runjava' /usr/share/geany/filedefs/filetypes.java"
t "CLion includes Python with the PyPy SDK" "grep -q /usr/local/bin/pypy3 ~/.config/JetBrains/CLion*/options/jdk.table.xml"
cd ~ && rm -rf ~/tn
echo; echo "$pass passed, $fail failed"
EOF
+3
View File
@@ -0,0 +1,3 @@
[Service]
# Test VM only (tests/check_vm.sh). Accepts SSH so the test tools can log in.
ExecStartPost=/usr/sbin/nft insert rule inet filter input tcp dport 22 accept
Executable
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
# Types text into the VM through the QEMU monitor (US keyboard).
# Usage: type.py MONITOR_SOCKET 'text' (newline = Enter; keys: {{ctrl-s}}, {{esc}}, ...)
import re, socket, sys, time
SHIFTED = {'!': '1', '@': '2', '#': '3', '$': '4', '%': '5', '^': '6', '&': '7', '*': '8',
'(': '9', ')': '0', '_': 'minus', '+': 'equal', '{': 'bracket_left',
'}': 'bracket_right', '|': 'backslash', ':': 'semicolon', '"': 'apostrophe',
'<': 'comma', '>': 'dot', '?': 'slash', '~': 'grave_accent'}
PLAIN = {' ': 'spc', '\n': 'ret', '\t': 'tab', '-': 'minus', '=': 'equal', '[': 'bracket_left',
']': 'bracket_right', '\\': 'backslash', ';': 'semicolon', "'": 'apostrophe',
',': 'comma', '.': 'dot', '/': 'slash', '`': 'grave_accent'}
def keys(text):
for token in re.split(r'(\{\{[a-z0-9_-]+\}\})', text):
if token.startswith('{{') and token.endswith('}}'):
yield token[2:-2]
continue
for c in token:
if c in PLAIN:
yield PLAIN[c]
elif c in SHIFTED:
yield 'shift-' + SHIFTED[c]
elif c.isupper():
yield 'shift-' + c.lower()
elif c.isalnum():
yield c
s = socket.socket(socket.AF_UNIX)
s.connect(sys.argv[1])
time.sleep(0.2)
text = open(sys.argv[2][1:]).read() if sys.argv[2].startswith('@') else sys.argv[2]
for k in keys(text):
s.sendall(('sendkey %s\n' % k).encode())
time.sleep(0.06)
time.sleep(0.3)
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Plugs a USB stick (32 MB, label TEAMUSB, one file) into the test VM started
# with --usb: tests/usb.sh plug|unplug
cd "$(dirname "$0")/.."
W=tests/work
case "$1" in
plug)
mkdir -p "$W/usb/content"
echo "prepared code library" > "$W/usb/content/lib.cpp"
truncate -s 32M "$W/usb/usb.img"
mkfs.ext4 -q -F -L TEAMUSB -d "$W/usb/content" "$W/usb/usb.img"
n=$(date +%s)
python3 -I tests/monitor.py "$W/monitor.sock" "drive_add 0 if=none,id=ud$n,file=$PWD/$W/usb/usb.img,format=raw"
python3 -I tests/monitor.py "$W/monitor.sock" "device_add usb-storage,bus=xhci.0,drive=ud$n,id=usbstick"
;;
unplug)
python3 -I tests/monitor.py "$W/monitor.sock" "device_del usbstick"
;;
esac
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Runs a command in the test VM: tests/vssh.sh team|icpc 'command'
# Passwords: TEAM_PASS (default contest) and ADMIN_PASS (password given to build.sh).
user=$1; shift
case "$user" in
team) pass=${TEAM_PASS:-contest} ;;
icpc) pass=${ADMIN_PASS:?set ADMIN_PASS to the administrator password} ;;
*) echo "user must be team or icpc" >&2; exit 2 ;;
esac
exec sshpass -p "$pass" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -o ConnectTimeout=5 -p 2222 "$user@127.0.0.1" "$@"
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
# Writes the contest image to USB sticks, all at the same time.
# ERASES the listed sticks. Only USB disks are accepted.
#
# sudo ./write_sticks.sh build/acpc2026.qcow2 /dev/sdb /dev/sdc ...
#
# The image can be the qcow2 file of build.sh (the ICPC image it is based on
# must stay where it was during the build) or a raw .img file.
set -uo pipefail
usage() {
echo "Usage: sudo $0 IMAGE DEVICE [DEVICE...]" >&2
echo >&2
echo "USB disks currently attached:" >&2
lsblk -d -o NAME,SIZE,MODEL,TRAN | awk 'NR == 1 || $NF == "usb"' >&2
exit 2
}
[ $# -ge 2 ] || usage
IMG=$1
shift
[ -f "$IMG" ] || { echo "Image not found: $IMG" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || { echo "Run this script as root (sudo)." >&2; exit 1; }
command -v qemu-img >/dev/null || { echo "qemu-img is missing." >&2; exit 1; }
FORMAT=$(qemu-img info --output=json "$IMG" | python3 -I -c 'import json, sys; print(json.load(sys.stdin)["format"])')
SIZE=$(qemu-img info --output=json "$IMG" | python3 -I -c 'import json, sys; print(json.load(sys.stdin)["virtual-size"])')
echo "Image: $IMG ($FORMAT, $((SIZE / 1000000000)) GB)"
echo
for dev in "$@"; do
[ -b "$dev" ] || { echo "Not a disk: $dev" >&2; exit 1; }
[ "$(lsblk -dn -o TYPE "$dev")" = disk ] || { echo "Not a whole disk: $dev" >&2; exit 1; }
[ "$(lsblk -dn -o TRAN "$dev")" = usb ] || { echo "Not a USB disk, refused: $dev" >&2; exit 1; }
if lsblk -n -o MOUNTPOINTS "$dev" | grep -q '[^[:space:]]'; then
echo "A partition of $dev is mounted, unmount it first." >&2
exit 1
fi
devsize=$(blockdev --getsize64 "$dev")
[ "$devsize" -ge "$SIZE" ] || { echo "$dev is too small ($((devsize / 1000000000)) GB)" >&2; exit 1; }
echo " $dev $(lsblk -dn -o SIZE,MODEL "$dev")"
done
echo
echo "ALL DATA ON THESE STICKS WILL BE ERASED."
read -rp "Type ERASE to continue: " answer
[ "$answer" = ERASE ] || { echo "Cancelled."; exit 1; }
logdir=$(mktemp -d)
write_one() {
local dev=$1 start=$(date +%s)
if qemu-img convert -n -f "$FORMAT" -O raw -t none "$IMG" "$dev" > "$logdir/$(basename "$dev").log" 2>&1; then
sync
echo " done $dev ($(( $(date +%s) - start )) s)"
else
echo " FAILED $dev (see $logdir/$(basename "$dev").log)"
return 1
fi
}
echo "Writing..."
fail=0
pids=()
for dev in "$@"; do
write_one "$dev" &
pids+=($!)
done
for pid in "${pids[@]}"; do wait "$pid" || fail=1; done
if [ $fail -eq 0 ]; then
echo "All sticks are written."
rm -rf "$logdir"
else
echo "At least one stick failed. Logs: $logdir"
fi
exit $fail