mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 03:30:36 +00:00
docs: add WSL WebUI autostart helpers
This commit is contained in:
@@ -118,6 +118,7 @@ The bootstrap will:
|
||||
5. Drop you into a first-run onboarding wizard inside the WebUI.
|
||||
|
||||
> Native Windows is not supported for this bootstrap yet. Use Linux, macOS, or WSL2.
|
||||
> For Windows / WSL auto-start at login, see [`docs/wsl-autostart.md`](docs/wsl-autostart.md).
|
||||
|
||||
If provider setup is still incomplete after install, the onboarding wizard will point you to finish it with `hermes model` instead of trying to replicate the full CLI setup in-browser.
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Windows / WSL auto-start
|
||||
|
||||
Hermes WebUI runs well under WSL2, but native Windows login does not automatically start Linux user processes. This guide covers two supported options:
|
||||
|
||||
1. **WSL session startup** — simple and low-risk. WebUI starts the next time you open a WSL shell.
|
||||
2. **Windows Task Scheduler** — true Windows logon startup. Windows invokes `wsl.exe`, which runs the WSL launch script.
|
||||
|
||||
Both paths use the same WSL launch script:
|
||||
|
||||
```text
|
||||
scripts/wsl/hermes_webui_autostart.sh
|
||||
```
|
||||
|
||||
The script is safe to call repeatedly. It uses a lock file, checks the `/health` endpoint, checks a pid file, and writes logs before starting `start.sh --foreground` in the background. It does not hardcode a user path; by default it derives the repository root from its own location.
|
||||
|
||||
## Script settings
|
||||
|
||||
The WSL launcher supports these environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `HERMES_WEBUI_REPO` | repo containing the script | WebUI checkout to start |
|
||||
| `HERMES_WEBUI_LOG_DIR` | `$HOME/.hermes/webui/logs` | Autostart and WebUI logs |
|
||||
| `HERMES_WEBUI_HOST` | `127.0.0.1` | Host passed through to `start.sh` / `bootstrap.py` |
|
||||
| `HERMES_WEBUI_PORT` | `8787` | WebUI port and health-check port |
|
||||
| `HERMES_WEBUI_HEALTH_URL` | `http://127.0.0.1:$HERMES_WEBUI_PORT/health` | URL used to decide whether WebUI is already running |
|
||||
| `HERMES_WEBUI_PID_FILE` | `$HERMES_WEBUI_LOG_DIR/hermes-webui.pid` | pid file used for duplicate prevention |
|
||||
| `HERMES_WEBUI_REQUIRE_AGENT_PROCESS` | `0` | Optional: set to `1` only if your local setup requires a separate Hermes process before WebUI starts |
|
||||
|
||||
Make the script executable once inside WSL:
|
||||
|
||||
```bash
|
||||
cd /path/to/hermes-webui
|
||||
chmod +x scripts/wsl/hermes_webui_autostart.sh
|
||||
```
|
||||
|
||||
Run it manually to verify your paths and logs:
|
||||
|
||||
```bash
|
||||
scripts/wsl/hermes_webui_autostart.sh
|
||||
curl -fsS http://127.0.0.1:8787/health
|
||||
```
|
||||
|
||||
Logs are written to:
|
||||
|
||||
```text
|
||||
$HOME/.hermes/webui/logs/webui_autostart.log
|
||||
$HOME/.hermes/webui/logs/hermes_webui.log
|
||||
```
|
||||
|
||||
## Option 1: WSL session startup
|
||||
|
||||
This starts WebUI when your WSL login shell starts. It is the easiest option if you already open WSL during your day.
|
||||
|
||||
Add this to `~/.profile` or `~/.bashrc` inside WSL, adjusting the repo path:
|
||||
|
||||
```bash
|
||||
if [ -x "$HOME/hermes-webui/scripts/wsl/hermes_webui_autostart.sh" ]; then
|
||||
HERMES_WEBUI_REPO="$HOME/hermes-webui" \
|
||||
"$HOME/hermes-webui/scripts/wsl/hermes_webui_autostart.sh" >/dev/null 2>&1 &
|
||||
fi
|
||||
```
|
||||
|
||||
Open a new WSL terminal and check:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:8787/health
|
||||
```
|
||||
|
||||
If you open several WSL terminals, the launcher should still start only one WebUI process because the lock, health check, and pid file all converge on "already running".
|
||||
|
||||
## Option 2: Windows Task Scheduler startup
|
||||
|
||||
Use this if you want WebUI to start automatically at Windows logon even before you open a WSL terminal.
|
||||
|
||||
The helper PowerShell script is:
|
||||
|
||||
```text
|
||||
scripts/windows/setup_webui_autostart.ps1
|
||||
```
|
||||
|
||||
From Windows PowerShell, run it with the WSL path to the launch script:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
.\scripts\windows\setup_webui_autostart.ps1 `
|
||||
-WslScriptPath "/home/your-user/hermes-webui/scripts/wsl/hermes_webui_autostart.sh" `
|
||||
-Distro "Ubuntu"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `-Distro` is optional. Omit it to use your default WSL distro.
|
||||
- The default task name is `HermesWebUIAutoStart`; pass `-TaskName` if you need a different name.
|
||||
- The script is idempotent: rerunning it updates the existing scheduled task instead of creating duplicates.
|
||||
- The task runs as the current Windows user at logon with least privilege.
|
||||
- Add `-WhatIf` to preview the scheduled task registration.
|
||||
- Add `-RunNow` to start the task immediately after registration.
|
||||
- Add `-SkipValidation` only if you need to register the task before the WSL path exists.
|
||||
|
||||
To inspect or remove the task later:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName HermesWebUIAutoStart
|
||||
Unregister-ScheduledTask -TaskName HermesWebUIAutoStart -Confirm:$false
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Check the WSL logs first:
|
||||
|
||||
```bash
|
||||
tail -n 80 "$HOME/.hermes/webui/logs/webui_autostart.log"
|
||||
tail -n 80 "$HOME/.hermes/webui/logs/hermes_webui.log"
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| Task exists but WebUI is not reachable | WSL script path is wrong for the selected distro | Re-run the PowerShell setup with the correct `-WslScriptPath` and `-Distro` |
|
||||
| WebUI starts only after opening WSL | You used the WSL session startup option, not Task Scheduler | Install the Windows scheduled task |
|
||||
| Multiple login events happen quickly | Normal Windows startup behavior | The WSL script should log `already running` and avoid duplicate processes |
|
||||
| Health check fails but pid exists | WebUI is still booting or the port differs | Check `HERMES_WEBUI_PORT` and `hermes_webui.log` |
|
||||
|
||||
If you want WSL2 systemd integration instead, see `docs/supervisor.md` for foreground process-supervisor guidance and adapt the Linux `systemd --user` pattern to your distro.
|
||||
@@ -0,0 +1,95 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$WslScriptPath,
|
||||
|
||||
[string]$Distro,
|
||||
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$TaskName = "HermesWebUIAutoStart",
|
||||
|
||||
[switch]$RunNow,
|
||||
|
||||
[switch]$SkipValidation
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function ConvertTo-WindowsArgument {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
if ($Value -notmatch '[\s\"]') {
|
||||
return $Value
|
||||
}
|
||||
|
||||
$escaped = $Value.Replace('"', '\"')
|
||||
return '"' + $escaped + '"'
|
||||
}
|
||||
|
||||
function Get-WslExePath {
|
||||
$systemWsl = Join-Path $env:SystemRoot "System32\wsl.exe"
|
||||
if (Test-Path $systemWsl) {
|
||||
return $systemWsl
|
||||
}
|
||||
return "wsl.exe"
|
||||
}
|
||||
|
||||
$wslExe = Get-WslExePath
|
||||
|
||||
$wslArgs = @()
|
||||
if ($Distro) {
|
||||
$wslArgs += @("-d", $Distro)
|
||||
}
|
||||
$wslArgs += @("--exec", "bash", $WslScriptPath)
|
||||
$actionArguments = ($wslArgs | ForEach-Object { ConvertTo-WindowsArgument -Value $_ }) -join " "
|
||||
|
||||
if (-not $SkipValidation) {
|
||||
$validationArgs = @()
|
||||
if ($Distro) {
|
||||
$validationArgs += @("-d", $Distro)
|
||||
}
|
||||
$validationArgs += @("--exec", "test", "-f", $WslScriptPath)
|
||||
|
||||
& $wslExe @validationArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "WSL script path was not found inside the selected distro: $WslScriptPath"
|
||||
}
|
||||
}
|
||||
|
||||
$description = "Auto-start Hermes WebUI inside WSL at Windows logon. Runs $WslScriptPath."
|
||||
$action = New-ScheduledTaskAction -Execute $wslExe -Argument $actionArguments
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel LeastPrivilege
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew
|
||||
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingTask) {
|
||||
Write-Host "Updating existing scheduled task '$TaskName'."
|
||||
} else {
|
||||
Write-Host "Creating scheduled task '$TaskName'."
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($TaskName, "Register Windows Scheduled Task for Hermes WebUI WSL autostart")) {
|
||||
Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Description $description `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Host "Task '$TaskName' is installed."
|
||||
Write-Host "Action: $wslExe $actionArguments"
|
||||
|
||||
if ($RunNow) {
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Write-Host "Task '$TaskName' started."
|
||||
}
|
||||
}
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# WSL-friendly autostart launcher for Hermes WebUI.
|
||||
#
|
||||
# Safe defaults:
|
||||
# - derives the repo from this script location, override with HERMES_WEBUI_REPO
|
||||
# - uses a lock + pid file to avoid duplicate starts
|
||||
# - treats a healthy /health endpoint as "already running"
|
||||
# - writes logs under ~/.hermes/webui/logs unless HERMES_WEBUI_LOG_DIR is set
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
HERMES_WEBUI_REPO="${HERMES_WEBUI_REPO:-${DEFAULT_REPO}}"
|
||||
HERMES_WEBUI_LOG_DIR="${HERMES_WEBUI_LOG_DIR:-${HOME}/.hermes/webui/logs}"
|
||||
HERMES_WEBUI_HOST="${HERMES_WEBUI_HOST:-127.0.0.1}"
|
||||
HERMES_WEBUI_PORT="${HERMES_WEBUI_PORT:-8787}"
|
||||
HERMES_WEBUI_HEALTH_HOST="${HERMES_WEBUI_HEALTH_HOST:-127.0.0.1}"
|
||||
HERMES_WEBUI_HEALTH_URL="${HERMES_WEBUI_HEALTH_URL:-http://${HERMES_WEBUI_HEALTH_HOST}:${HERMES_WEBUI_PORT}/health}"
|
||||
HERMES_WEBUI_PID_FILE="${HERMES_WEBUI_PID_FILE:-${HERMES_WEBUI_LOG_DIR}/hermes-webui.pid}"
|
||||
HERMES_WEBUI_LOCK_FILE="${HERMES_WEBUI_LOCK_FILE:-/tmp/hermes-webui-autostart.lock}"
|
||||
AUTOSTART_LOG="${HERMES_WEBUI_LOG_DIR}/webui_autostart.log"
|
||||
WEBUI_LOG="${HERMES_WEBUI_LOG_DIR}/hermes_webui.log"
|
||||
|
||||
# Make the WSL launcher knobs visible to start.sh/bootstrap.py.
|
||||
export HERMES_WEBUI_HOST HERMES_WEBUI_PORT
|
||||
|
||||
mkdir -p "${HERMES_WEBUI_LOG_DIR}"
|
||||
chmod 700 "${HERMES_WEBUI_LOG_DIR}" 2>/dev/null || true
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "$*" | tee -a "${AUTOSTART_LOG}"
|
||||
}
|
||||
|
||||
webui_healthy() {
|
||||
command -v curl >/dev/null 2>&1 \
|
||||
&& curl -fsS --max-time 3 "${HERMES_WEBUI_HEALTH_URL}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pid_is_alive() {
|
||||
[[ -s "${HERMES_WEBUI_PID_FILE}" ]] || return 1
|
||||
local pid
|
||||
pid="$(cat "${HERMES_WEBUI_PID_FILE}" 2>/dev/null || true)"
|
||||
[[ "${pid}" =~ ^[0-9]+$ ]] || return 1
|
||||
kill -0 "${pid}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
validate_repo() {
|
||||
if [[ ! -d "${HERMES_WEBUI_REPO}" ]]; then
|
||||
log "Hermes WebUI repo not found: ${HERMES_WEBUI_REPO}"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${HERMES_WEBUI_REPO}/start.sh" ]]; then
|
||||
log "start.sh not found under HERMES_WEBUI_REPO=${HERMES_WEBUI_REPO}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_require_agent_process() {
|
||||
# Hermes WebUI usually launches the agent in-process, so this check is opt-in.
|
||||
# Set HERMES_WEBUI_REQUIRE_AGENT_PROCESS=1 only if your setup depends on a
|
||||
# separately running Hermes gateway/agent before WebUI starts.
|
||||
if [[ "${HERMES_WEBUI_REQUIRE_AGENT_PROCESS:-0}" != "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! pgrep -f "hermes" >/dev/null 2>&1; then
|
||||
log "HERMES_WEBUI_REQUIRE_AGENT_PROCESS=1 but no Hermes process is running; skipping start"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
acquire_lock() {
|
||||
exec 9>"${HERMES_WEBUI_LOCK_FILE}"
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
if ! flock -n 9; then
|
||||
log "Autostart already running; lock held at ${HERMES_WEBUI_LOCK_FILE}"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
log "flock not found; continuing without lock-based duplicate protection"
|
||||
fi
|
||||
}
|
||||
|
||||
start_webui() {
|
||||
validate_repo
|
||||
maybe_require_agent_process
|
||||
|
||||
if webui_healthy; then
|
||||
log "Hermes WebUI already running at ${HERMES_WEBUI_HEALTH_URL}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if pid_is_alive; then
|
||||
log "Hermes WebUI already running with pid $(cat "${HERMES_WEBUI_PID_FILE}")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rm -f "${HERMES_WEBUI_PID_FILE}"
|
||||
log "Starting Hermes WebUI from ${HERMES_WEBUI_REPO} on ${HERMES_WEBUI_HOST}:${HERMES_WEBUI_PORT}"
|
||||
|
||||
(
|
||||
cd "${HERMES_WEBUI_REPO}"
|
||||
nohup bash "${HERMES_WEBUI_REPO}/start.sh" --foreground >>"${WEBUI_LOG}" 2>&1 &
|
||||
printf '%s\n' "$!" >"${HERMES_WEBUI_PID_FILE}"
|
||||
)
|
||||
|
||||
sleep "${HERMES_WEBUI_STARTUP_GRACE_SECONDS:-2}"
|
||||
if webui_healthy; then
|
||||
log "Hermes WebUI started and passed health check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if pid_is_alive; then
|
||||
log "Hermes WebUI process started with pid $(cat "${HERMES_WEBUI_PID_FILE}"); health check not ready yet"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Hermes WebUI failed to stay running; see ${WEBUI_LOG}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
acquire_lock
|
||||
start_webui
|
||||
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
DOC = REPO_ROOT / "docs" / "wsl-autostart.md"
|
||||
WSL_SCRIPT = REPO_ROOT / "scripts" / "wsl" / "hermes_webui_autostart.sh"
|
||||
POWERSHELL_SCRIPT = REPO_ROOT / "scripts" / "windows" / "setup_webui_autostart.ps1"
|
||||
README = REPO_ROOT / "README.md"
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_wsl_autostart_docs_cover_session_and_task_scheduler_options():
|
||||
doc = _read(DOC)
|
||||
readme = _read(README)
|
||||
|
||||
assert "docs/wsl-autostart.md" in readme
|
||||
assert "WSL session startup" in doc
|
||||
assert "Windows Task Scheduler" in doc
|
||||
assert "scripts/wsl/hermes_webui_autostart.sh" in doc
|
||||
assert "scripts/windows/setup_webui_autostart.ps1" in doc
|
||||
assert "HERMES_WEBUI_REPO" in doc
|
||||
assert "HERMES_WEBUI_LOG_DIR" in doc
|
||||
assert "HERMES_WEBUI_REQUIRE_AGENT_PROCESS" in doc
|
||||
assert "/root" not in doc
|
||||
assert "C:\\Users\\Michael" not in doc
|
||||
|
||||
|
||||
def test_wsl_autostart_launcher_has_safe_duplicate_prevention_and_exports_runtime_env():
|
||||
script = _read(WSL_SCRIPT)
|
||||
|
||||
assert script.startswith("#!/usr/bin/env bash\n")
|
||||
assert "set -euo pipefail" in script
|
||||
assert "flock -n" in script
|
||||
assert "HERMES_WEBUI_LOCK_FILE" in script
|
||||
assert "HERMES_WEBUI_PID_FILE" in script
|
||||
assert "curl -fsS --max-time 3" in script
|
||||
assert "bash \"${HERMES_WEBUI_REPO}/start.sh\" --foreground" in script
|
||||
assert "nohup" in script
|
||||
|
||||
# The launcher documents HERMES_WEBUI_HOST/PORT as runtime knobs; they must
|
||||
# be exported so bootstrap.py/server.py receive the selected WSL values.
|
||||
assert re.search(r"^export HERMES_WEBUI_HOST HERMES_WEBUI_PORT$", script, re.MULTILINE)
|
||||
|
||||
assert "/root" not in script
|
||||
assert "/home/michael" not in script
|
||||
|
||||
|
||||
def test_wsl_autostart_launcher_passes_bash_syntax_check():
|
||||
subprocess.run(["bash", "-n", str(WSL_SCRIPT)], check=True, cwd=REPO_ROOT)
|
||||
|
||||
|
||||
def test_windows_task_scheduler_helper_is_idempotent_and_validates_wsl_script_path():
|
||||
script = _read(POWERSHELL_SCRIPT)
|
||||
|
||||
assert "[CmdletBinding(SupportsShouldProcess = $true)]" in script
|
||||
assert "Register-ScheduledTask" in script
|
||||
assert "-Force" in script
|
||||
assert "New-ScheduledTaskSettingsSet" in script
|
||||
assert "-MultipleInstances IgnoreNew" in script
|
||||
assert "Get-ScheduledTask -TaskName $TaskName" in script
|
||||
assert "wsl.exe" in script
|
||||
assert '"--exec", "bash", $WslScriptPath' in script
|
||||
assert '"--exec", "test", "-f", $WslScriptPath' in script
|
||||
assert "Start-ScheduledTask -TaskName $TaskName" in script
|
||||
assert "/root" not in script
|
||||
assert "C:\\Users\\Michael" not in script
|
||||
|
||||
|
||||
def test_powershell_helper_passes_parser_when_pwsh_is_available():
|
||||
pwsh = None
|
||||
for candidate in ("pwsh", "powershell"):
|
||||
result = subprocess.run(["bash", "-lc", f"command -v {candidate}"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
pwsh = result.stdout.strip()
|
||||
break
|
||||
if not pwsh:
|
||||
# Linux CI often does not include PowerShell. The source-string tests
|
||||
# above still pin the safety/idempotency invariants in that case.
|
||||
return
|
||||
|
||||
subprocess.run(
|
||||
[pwsh, "-NoProfile", "-Command", f"$null = [scriptblock]::Create((Get-Content -Raw '{POWERSHELL_SCRIPT.as_posix()}'))"],
|
||||
check=True,
|
||||
cwd=REPO_ROOT,
|
||||
)
|
||||
Reference in New Issue
Block a user