- Rust 55%
- TypeScript 26.2%
- CSS 8.1%
- Python 6.9%
- JavaScript 3.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| guest | ||
| public | ||
| runtime | ||
| scripts | ||
| src | ||
| src-tauri | ||
| .gitattributes | ||
| .gitignore | ||
| bun.lock | ||
| index.html | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| rust-toolchain.toml | ||
| THIRD_PARTY.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
ExamLab
A desktop or centrally hosted teacher app for coding exams. Share one link; every student gets an isolated Linux environment with VS Code, selected language tools, their own workspace disk, and an optional exam timer.
The desktop app uses Rust, Tauri 2, React, TypeScript, and QEMU. Docker and WSL are not runtime dependencies.
Run the app
Development requires Bun, a stable Rust toolchain, and the Tauri development prerequisites for your OS. These are build requirements, not requirements for teachers using a packaged release.
bun install
bun run desktop:dev
For a browser-based teacher interface during development, run these in separate terminals:
bun run host
bun run dev
Open http://127.0.0.1:1420. Build the frontend with bun run build before compiling the Rust host to embed the interface, which is also served directly at http://127.0.0.1:4747.
Build a desktop installer:
bun run desktop:build
The release bundle is written to src-tauri/target/release/bundle/. Windows installation is per-user. The Linux AppImage bundles its desktop libraries; use --appimage-extract-and-run on systems without FUSE. The generated .deb declares its system WebKitGTK dependencies. A portable archive of the generated ExamLab.AppDir can also be extracted and launched with AppRun, without FUSE or package installation.
Linux packaging also requires squashfs-tools (mksquashfs). bun run desktop:build removes the bundled Wayland client from both the AppImage and portable AppDir so the host's Mesa/EGL graphics driver loads its matching system library. This prevents WebKit startup crashes with Could not create default EGL display: EGL_BAD_PARAMETER on newer Linux systems. To repair an older unsigned AppImage in place, first make a backup, then run bun scripts/fix-appimage.mjs /path/to/ExamLab.AppImage.
Remote management
Launch the same app with --remote to run without creating a desktop window:
# Windows (PowerShell waits for a GUI executable when piped)
.\exam-lab.exe --remote | Out-Host
# Linux portable bundle
./ExamLab.AppDir/AppRun --remote
# Or the installed desktop executable
exam-lab --remote
Open http://<server-address>:4747 from a teacher's browser. Remote mode binds management to 0.0.0.0:4747; student ingress still uses port 8080 by default. Allow these ports through the server's firewall. QEMU, exam preparation, and student VMs all run on the server. Virtualization and permissions must be configured there; interactive system repair is available in desktop mode.
The terminal displays a teacher access key on startup. The generated key persists in remote-access-key under the application data directory. Alternatively, set EXAMLAB_REMOTE_KEY to a secret containing at least 32 printable characters without spaces. Teachers enter this key to sign in. Browser sign-in lasts for the tab's session; restarting the server requires signing in again. The access key gives full administration of the shared instance: all signed-in teachers can see and manage all exams. Separate teacher accounts and tenant isolation are not implemented.
Directory paths and the browser's folder picker refer to the server's filesystem, including starter files and saved student work. Files on a teacher's laptop must be copied to the server before selecting them. Remote mode does not launch server file explorer windows or interactive privilege-elevation prompts. Student links use the server hostname opened in the teacher's browser and the configured ingress port; that address must also be reachable by students.
For an internet-facing deployment, put management behind an HTTPS reverse proxy and preserve the original Host header. HTTP itself does not encrypt access keys or exam data. Student ingress needs separate routing to its port, including WebSocket forwarding. Stop the server with Ctrl+C or, on Linux, SIGTERM to save student work and shut down VMs normally.
The UI is embedded in the executable; deploying it does not require the source checkout or a separate dist directory. For a Linux server without desktop libraries, use the exam-host-linux-x86_64 release or build the headless binary:
bun run build
cargo build --release --manifest-path src-tauri/Cargo.toml --bin exam-host
./src-tauri/target/release/exam-host --remote
Teacher workflow
- Open Setup. On first launch, QEMU and all its libraries are downloaded into ExamLab's private application folder. Downloads are checksum-verified before execution. A packaged release can include the runtime to avoid that download.
- If needed, choose Repair system setup. Windows needs Windows Hypervisor Platform and firmware virtualization. Linux needs a working KVM module and access to
/dev/kvm. Only the system repair helper requests elevation; the teacher app and VMs run under the original user. - Create an exam and choose where student work should be stored. Configure VS Code using the searchable Settings tab or paste a configuration in the JSON tab. Both views preserve custom settings. Search Open VSX for extensions or enter IDs directly; search selections pin the displayed version. Choose language toolchains and additional Ubuntu APT packages (optionally
package=version), terminal availability, internet access, time limits, VM resources, starter folders, identification, a join password, and student capacity. - Prepare the exam environment before class. This downloads an Ubuntu image and installs code-server, selected tools, and extensions. Preparation requires internet access on the teacher computer, even for an offline exam. Reusable environments are cached by their tooling and guest-agent fingerprint.
- Open exam and share its entry URL, for example
http://192.168.1.50:8080/ingress/<exam-id>. The shorter/ingressroute redirects automatically when exactly one exam is open, or lists the available exams. - Students identify themselves if configured, then wait for their VM to start. Refreshes and temporary browser disconnects reconnect to the same session using a private browser cookie.
- Follow student sessions in the teacher app. Pause new joins leaves existing sessions running. End exam saves and stops them. Exam configuration cannot change while active sessions exist.
Toolchain presets include Python, Java, JavaScript/TypeScript, Rust, Go, C/C++, C#/.NET, Ruby, PHP, Lua, Perl, R, and Haskell. Custom packages come from the guest Ubuntu 24.04 repositories; enter names separated by spaces, commas, or newlines. A custom package version overrides an unpinned preset package. Missing packages or unavailable versions cause preparation to fail with the build log available in the image cache. Selected packages and extensions remain available during offline exams. Open VSX search requires internet access on the teacher computer.
Selecting a language preset offers its recommended Open VSX extensions with Add extensions and Skip. Python offers Microsoft Python, Python Debugger, and Ruff. Existing extension selections and version pins are preserved; removing a language never removes extensions. Preset recommendations use unpinned IDs, so preparation installs the registry's current version; use search or By ID to pin a version.
The student workspace fills the browser window. Click the ExamLab timer in VS Code's bottom-left status bar to see the exam description, student name, time limit and deadline, terminal/internet rules, resources, languages, packages, and extensions.
Preparation shows the current installation step, elapsed time, and a live, scrollable log. Package names, version pins, and dependencies are checked against the VM's Ubuntu package lists before installation. Installer failures stop the setup VM and report the error immediately; use Retry after correcting the package or extension selection. Logs remain available after a failed build.
The host computer must stay awake and reachable. Allow the student ingress port on the classroom network. Desktop management binds to loopback; remote management requires the teacher access key. Management APIs require an app-generated bearer token and reject foreign browser origins in both modes.
Data and recovery
Each student gets a uniquely named directory under the chosen exam directory:
student-name-1234abcd/
work/ # ordinary student files, mirrored to the host
.exam-lab/
workspace.qcow2 # authoritative persistent ext4 workspace disk
config.img # private per-session guest configuration
sync.json # mirror metadata
serial.log
qemu.log
The work folder syncs approximately every ten seconds. Ending a session freezes student processes, takes a final mirror, flushes the guest, and shuts it down. Student workspace disks remain even if a mirror fails. Files over 256 MB, symbolic links, or names incompatible with the host are preserved on the guest disk but are not mirrored as ordinary host files. The teacher UI reports mirror failures.
Deleting an exam configuration does not delete student data. App configuration writes are atomic, and a file lock prevents two app instances from editing the same state. An unexpected app exit marks running sessions interrupted on the next launch. Reconnecting recovers their disk and original deadline. A normal app exit saves and finishes active sessions.
No teacher folders are mounted into guests. Starter folders are copied in background provisioning, then streamed to the VM as one archive. The join page responds before copying begins, and students see the current provisioning stage. Symlinks are skipped; the student data directory must be outside the distributed folders. Student names never become unchecked host paths; mirror paths are validated and host symlinks are rejected.
Isolation, settings, and timing
- Linux: QEMU's
microvmmachine, KVM, a directly booted prepared kernel/initramfs, and virtio devices. - Windows: A minimal headless
q35machine using WHPX. It uses the same prepared Linux environment. Themicrovmmachine is not treated as a universal WHPX-compatible backend. - The system image is read-only during student sessions. Student data and user caches live on the separate bounded workspace disk. The student account has no sudo access.
- Offline guests use user-mode networking with
restrict=on. Only two explicit loopback forwards remain: the editor and the authenticated guest management service. No TAP device or privileged networking setup is required. - RAM and disk allocations are fixed. vCPU count limits guest parallelism. Windows Job Objects additionally cap host process CPU/memory and terminate orphaned VMs. Linux applies cgroup v2 CPU/memory caps where the launching user has a delegated hierarchy; the UI reports that host caps are conditional. On Linux, the VM also receives a parent-death signal and QEMU's seccomp sandbox.
- There is no TCG fallback for student sessions. Hardware virtualization is required.
- Teacher settings are written to protected user and initial workspace settings. A bundled VS Code extension reapplies managed values to newly opened workspace folders, displays the countdown in VS Code's status bar, and removes newly opened terminals when the terminal is disabled. Terminal disabling controls editor functionality; language tools and student programs can still execute code. The managed extension is editor policy, not an additional OS security boundary.
- The timer starts on the first successful editor HTTP/WebSocket connection, after provisioning. A refresh never resets it. The host persists and enforces the deadline; VS Code displays the countdown, and the browser shows the completion screen at expiry. Browser clock changes do not extend a session. At expiry, proxy access closes and the VM is saved and stopped.
This release targets x86-64 Windows and Linux. Arm hosts are explicitly reported as unsupported. LAN ingress uses HTTP as requested; keep it on the intended classroom network.
Runtime distribution
Pinned runtime sources and checksums are in src-tauri/src/runtime.rs:
- Windows: Stefan Weil's QEMU distribution, extracted with official 7-Zip binaries. The QEMU installer is never executed; no registry entries, drivers, or system services are installed.
- Linux: pkgforge's portable QEMU AppImage, extracted into the private runtime directory without FUSE.
Place a pre-extracted runtime in runtime/windows-x86_64/ or runtime/linux-x86_64/ before building to ship it with the installer. Include the complete required libraries, firmware, executable permissions, and third-party notices. See THIRD_PARTY.md.
App files default to the platform's local application-data directory. Development overrides:
| Variable | Purpose |
|---|---|
EXAMLAB_DATA_DIR |
App configuration, private runtime, and shared image cache |
EXAMLAB_INGRESS_PORT |
Student listener port; defaults to 8080 |
EXAMLAB_REMOTE_KEY |
Override the persistent teacher access key in remote mode |
EXAMLAB_UI_DIR |
Optional directory overriding the embedded management web assets |
RUST_LOG |
Rust logging configuration |
Checks
bun run build
bun run test
cargo test --manifest-path src-tauri/Cargo.toml
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --features desktop -- -D warnings
On Windows, stop a running development exam-host.exe before rebuilding or testing binaries, because Windows locks executing .exe files. cargo check and cargo test --lib can run alongside the host.
Guest archive validation checks run with cd guest && python3 -m unittest test_agent on Linux.
Tests cover management authentication/origin checks, filesystem containment, atomic saves, timer persistence and expiration, app-state locking, editor JSON validation, and image-cache invalidation. Hardware virtualization and network policies should also be validated on the actual classroom machines before a live exam.
An opt-in hardware test boots two real guests, checks project distribution and guest authentication, mirrors an answer, and verifies disk recovery after shutdown. Point it at a prepared environment with a resolvable backing image:
EXAMLAB_TEST_IMAGE=/path/to/images/<environment-key> \
EXAMLAB_TEST_RUNTIME=/path/to/app/runtime \
cargo test --manifest-path src-tauri/Cargo.toml --test hardware -- --ignored
On PowerShell, set those two variables with $env:EXAMLAB_TEST_IMAGE and $env:EXAMLAB_TEST_RUNTIME before running the Cargo command. The runtime variable is optional; if omitted, the test downloads a private verified runtime. Guest settings files use read-only bind mounts so VS Code can still write its logs, IPC sockets, and session state.
A second opt-in hardware check exercises bulk starter transfer and a nonblocking join. Set EXAMLAB_TEST_DATA to an unused app data directory with QEMU/base-image caches and EXAMLAB_TEST_STARTER to a test project containing 20,000 small files, main.py with print(42) followed by a newline, and a 300 MiB large-resource.bin. Run cargo test --manifest-path src-tauri/Cargo.toml --test hardware large_starter_projects -- --ignored --nocapture. The check creates and removes its own exam/session configuration while preserving the test workspace. After updating the app, recheck/prepare an exam environment to install the updated guest agent before opening it.
To verify package preflight with real VMs, set EXAMLAB_TEST_DATA to an unused app data directory with QEMU and Ubuntu base-image caches, then run cargo test --manifest-path src-tauri/Cargo.toml --test hardware unavailable_packages -- --ignored --nocapture. This checks a missing package and an unavailable version pin, including the readable preparation-log endpoint.
Failed student workspaces have a Retry action in both student lists. Before the first editor connection, retry uses the current exam settings and preserves the previous attempt in .exam-lab-failed-<id> inside the same student folder. The existing student link resumes automatically. Once a student has started, retry keeps their workspace, original settings, and deadline.
The VS Code settings page includes a Disable AI features switch, enabled for new exams and older configurations that have no explicit choice. It sets chat.disableAIFeatures and stays synchronized with JSON. While disabled, ExamLab also hides welcome shortcuts because VS Code can cache the Open Chat prompt; other editor settings stay available. Click the ExamLab status item to open a compact, dismissible exam-details popup with a live countdown.