- Python 94.8%
- Just 5.2%
| src | ||
| .gitignore | ||
| .python-version | ||
| beowulfd.service | ||
| example_beowulfd.service | ||
| example_beowulfdrc | ||
| example_firefox.desktop | ||
| example_hosts | ||
| justfile | ||
| main.py | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Beowulf
A tool for delegating GUI applications to multiple backend hosts using waypipe.
Overview
Beowulf consists of two components:
- beowulf - Client tool that launches GUI applications on remote hosts
- beowulfd - Daemon that manages host allocation based on resource usage
How It Works
- You run
beowulf "firefox https://en.wikipedia.org/" - The client asks the daemon which host should run the application
- The daemon analyzes historical resource usage and current host metrics to make an intelligent allocation decision
- The client launches the application via waypipe on the chosen host
- The application runs remotely but appears as a local window
- The daemon tracks resource usage for future allocation decisions
Installation
Prerequisites
- Python 3.11 or higher
waypipeinstalled and configured- SSH access to remote hosts with key-based authentication
- Remote hosts need a working
/procfilesystem along withpython3,ps, andpgrep
Install
# Clone or navigate to the beowulf directory
cd beowulfd
# Install with uv
uv pip install -e .
Configuration
1. Create hosts file
Create ~/.beowulf/hosts with one hostname per line:
# Format: hostname or hostname:port
localhost
workstation.local
server1.example.com
server2.example.com:2222
Important: All hosts must already have their SSH host keys in your ~/.ssh/known_hosts file.
2. (Optional) Create daemon configuration
Create ~/.beowulf/beowulfdrc to customize behavior:
# Polling interval in seconds (default: 10)
polling_interval = 10
# Sticky assignments (always run on specific host)
sticky.firefox = workstation.local
sticky.gimp = server1.example.com
Usage
Start the daemon
beowulfd
# Inspect daemon options
beowulfd --help
The daemon will:
- Create the
~/.beowulfdirectory structure - Create a Unix socket at
~/.beowulf/run/daemon - Connect to all configured hosts via SSH
- Begin monitoring resource usage
- Log activity to stdout
- Expose a Plotly-based metrics dashboard on http://127.0.0.1:8080 by default
Visit http://127.0.0.1:8080 (or your configured host/port) to inspect per-host CPU, memory, and iowait graphs collected over time.
Tip: Run under a supervisor like systemd or in a tmux/screen session.
Launch applications
# Basic usage
beowulf "firefox https://en.wikipedia.org/"
# Any command works
beowulf "gimp"
beowulf "inkscape drawing.svg"
# Suppress client diagnostics (only show app stdout/stderr)
beowulf --silent "firefox https://en.wikipedia.org/"
# Run CLI applications over SSH (no waypipe)
beowulf --cli "htop"
# Check allocation without launching (dry-run)
beowulf --dry-run "firefox"
# View client help
beowulf --help
The beowulf command will:
- Block for the entire lifetime of the application
- Forward signals (SIGINT, SIGTERM) to the remote process
- Exit with the same exit code as the remote application
- Fail gracefully if the daemon is not running
- Use
waypipefor GUI applications, or--clifor TUI/CLI programs
Use in Desktop Entry files
Beowulf can be used in .desktop files for GNOME, KDE, etc:
[Desktop Entry]
Name=Firefox (Beowulf)
Exec=beowulf "firefox %u"
Type=Application
Terminal=false
Metrics dashboard
Beowulfd now exposes a read-only Plotly dashboard showing CPU, memory, and I/O wait trends for every connected host. By default it listens on 127.0.0.1:8080, but you can change this with --http-host and --http-port, or disable it entirely with --disable-http.
The dashboard renders static graphs for the most recent samples collected while the daemon runs, so leave the service online to accumulate history. Plotly's JavaScript runtime is bundled directly with the daemon, so the page works without internet access.
Allocation Strategy
The daemon makes intelligent allocation decisions based on:
- Historical data: Apps that historically use more memory are allocated to hosts with sufficient free memory
- Current resources: CPU usage, memory availability, and I/O wait times
- I/O wait penalty: Hosts with high I/O wait are deprioritized even if resources are available
- Sticky configuration: Specific apps can be pinned to specific hosts
- Fallback: Unknown apps go to the host with the most free resources
Protocol
Communication between client and daemon uses JSON over Unix socket:
// Client - Daemon: Request allocation
{"cmd": "allocate", "app": "firefox"}
// Daemon - Client: Respond with host
{"host": "host1.local", "reason": "lowest iowait"}
// Client - Daemon: Report launched process
{"cmd": "report", "app": "firefox", "pid": 12345, "host": "host1.local"}
Architecture
beowulfd (Daemon)
- Maintains persistent SSH connections to all configured hosts
- Polls resource metrics every N seconds (configurable)
- Stores metrics in TinyFlux time series database
- Tracks running processes and their resource usage
- Provides allocation API via Unix socket
- Process names are normalized to first token (e.g., "firefox-esr" - "firefox")
beowulf (Client)
- Connects to daemon via Unix socket
- Requests host allocation for an application
- Launches application via
waypipe ssh HOST COMMAND - Attempts to determine remote PID and reports back to daemon
- Blocks until application exits
- Returns same exit code as remote application
Data Storage
Resource metrics are stored in ~/.beowulf/metrics.db (TinyFlux database):
- Host metrics:
host.HOSTNAME- CPU, memory, I/O wait - App metrics:
app.APPNAME- Per-process CPU and memory usage
Process data is only counted when the process is running (no zero values).
Troubleshooting
Daemon won't connect to hosts
Check:
- SSH keys are set up correctly:
ssh host commandshould work without password - Host keys are in
~/.ssh/known_hosts: Runssh hostonce manually first - Firewall allows SSH connections
Client says "daemon is not running"
- Start the daemon:
beowulfd - Check socket exists:
ls ~/.beowulf/run/daemon - Check daemon logs for errors
Application doesn't launch
- Verify waypipe works:
waypipe ssh host firefoxshould work manually - Check daemon logs for allocation errors
- Try dry-run mode:
beowulf --dry-run "firefox"
Remote PID not detected
The client uses pgrep to find the remote PID. If this fails:
- Process tracking won't work, but the app will still run
- This is a warning, not a fatal error
- Check that
pgrepis available on remote host
Security
- Unix socket permissions are set to owner-only (600)
- Only the user running beowulfd can use beowulf
- All communication happens locally via Unix socket
- SSH connections use your existing SSH configuration and keys
Example Session
# Terminal 1: Start daemon
$ beowulfd
2025-11-11 17:53:00 - beowulfd - INFO - Loaded 2 hosts
2025-11-11 17:53:00 - beowulfd - INFO - Connected to localhost:22
2025-11-11 17:53:00 - beowulfd - INFO - Connected to workstation.local:22
2025-11-11 17:53:00 - beowulfd - INFO - Connected to 2/2 hosts
2025-11-11 17:53:00 - beowulfd - INFO - Socket server listening on /home/user/.beowulf/run/daemon
2025-11-11 17:53:00 - beowulfd - INFO - Starting monitoring loop (interval: 10.0s)
# Terminal 2: Launch application
$ beowulf "firefox https://en.wikipedia.org/"
Allocated to workstation.local: low CPU usage, high available memory
Launching: waypipe ssh workstation.local firefox https://en.wikipedia.org/
Reported PID 12345 to daemon
# Firefox opens as a window, beowulf blocks until you close it
License
MIT