Fix agent install/uninstall sudo placement, add dependency install hints

curl ... | API_URL=... bash runs as the invoking user, not root, so
"sudo" has to go right after the pipe (elevating bash) rather than
before curl — the old commands in the app UI, README, and scripts'
own usage comments put it in a spot that still failed the root check.
Also have install.sh/report-tasks.sh suggest the right package-manager
command (apt/dnf/yum/apk/pacman/zypper) when curl or jq is missing
instead of just failing, and bring the README up to date with both.
This commit is contained in:
2026-09-02 23:12:12 +02:00
parent 0615866311
commit 329dcab936
5 changed files with 71 additions and 18 deletions
+12 -6
View File
@@ -11,8 +11,9 @@ support is a documented future addition — see [`agent/windows/README.md`](agen
- `server/` — Express + TypeScript API, SQLite (via libSQL) storage, Authentik OIDC login. - `server/` — Express + TypeScript API, SQLite (via libSQL) storage, Authentik OIDC login.
- `web/` — React + Vite single-page app, served as static files by the Express server. - `web/` — React + Vite single-page app, served as static files by the Express server.
- `agent/linux/` — `report-tasks.sh` (collects cron + systemd timer data) and - `agent/linux/` — `report-tasks.sh` (collects cron + systemd timer data),
`install.sh` (installs it as a systemd timer on a target server). `install.sh` (installs it as a systemd timer on a target server), and
`uninstall.sh` (removes it).
Data model, API contract, and sync behavior are documented in code: Data model, API contract, and sync behavior are documented in code:
[`server/src/db/schema.ts`](server/src/db/schema.ts), [`server/src/db/schema.ts`](server/src/db/schema.ts),
@@ -64,11 +65,13 @@ automatically on server start.
1. Sign in, go to **Servers**, and add a server. The generated API token is 1. Sign in, go to **Servers**, and add a server. The generated API token is
shown once — copy it. shown once — copy it.
2. On the target Linux server (as root): 2. On the target Linux server, as root. If you're not already root, put
`sudo` right after the pipe so it elevates `bash`, not `curl` — `sudo curl
... | bash` would still fail the root check:
```sh ```sh
curl -fsSL https://schedule.example.lan/agent/linux/install.sh | \ curl -fsSL https://schedule.example.lan/agent/linux/install.sh | \
API_URL=https://schedule.example.lan API_TOKEN=stm_xxx bash sudo API_URL=https://schedule.example.lan API_TOKEN=stm_xxx bash
``` ```
This installs `report-tasks.sh` to `/usr/local/bin`, writes the API This installs `report-tasks.sh` to `/usr/local/bin`, writes the API
@@ -76,7 +79,10 @@ automatically on server start.
`schedule-task-manager-agent.timer` systemd timer that reports every 15 `schedule-task-manager-agent.timer` systemd timer that reports every 15
minutes (override with `INTERVAL_MINUTES=5` before the `bash` above). minutes (override with `INTERVAL_MINUTES=5` before the `bash` above).
Requires `curl`, `jq`, and `systemd` on the target server. Requires `curl`, `jq`, and `systemd` on the target server — if either
`curl` or `jq` is missing, both this script and `report-tasks.sh` print
the right install command for the server's package manager (apt, dnf,
yum, apk, pacman, or zypper) and exit.
3. Tasks appear on the Dashboard, grouped by server and then by schedule type, 3. Tasks appear on the Dashboard, grouped by server and then by schedule type,
within one reporting interval. within one reporting interval.
@@ -88,7 +94,7 @@ hidden by default — toggle "Show stale/missing tasks" to see them.
To remove the agent from a server, run: To remove the agent from a server, run:
```sh ```sh
curl -fsSL https://schedule.example.lan/agent/linux/uninstall.sh | bash curl -fsSL https://schedule.example.lan/agent/linux/uninstall.sh | sudo bash
``` ```
This stops and deletes the systemd timer/service, the installed script, and This stops and deletes the systemd timer/service, the installed script, and
+31 -5
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Installs the Schedule Task Manager agent as a systemd timer. # Installs the Schedule Task Manager agent as a systemd timer.
# #
# Usage: # Usage (must run as root — if not already root, put sudo right after the
# pipe so it applies to bash, not to curl):
# curl -fsSL https://schedule.example.lan/agent/linux/install.sh | \ # curl -fsSL https://schedule.example.lan/agent/linux/install.sh | \
# API_URL=https://schedule.example.lan API_TOKEN=stm_xxx bash # sudo API_URL=https://schedule.example.lan API_TOKEN=stm_xxx bash
# #
set -euo pipefail set -euo pipefail
@@ -17,16 +18,41 @@ INTERVAL_MINUTES="${INTERVAL_MINUTES:-15}"
if [[ "$EUID" -ne 0 ]]; then if [[ "$EUID" -ne 0 ]]; then
echo "This installer must be run as root (it installs a systemd timer)." >&2 echo "This installer must be run as root (it installs a systemd timer)." >&2
echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2
echo " curl -fsSL ... | sudo API_URL=... API_TOKEN=... bash" >&2
exit 1 exit 1
fi fi
for bin in curl jq systemctl; do suggest_package_install() {
local pkg="$1"
if command -v apt-get >/dev/null 2>&1; then
echo " sudo apt-get update && sudo apt-get install -y $pkg"
elif command -v dnf >/dev/null 2>&1; then
echo " sudo dnf install -y $pkg"
elif command -v yum >/dev/null 2>&1; then
echo " sudo yum install -y $pkg"
elif command -v apk >/dev/null 2>&1; then
echo " sudo apk add $pkg"
elif command -v pacman >/dev/null 2>&1; then
echo " sudo pacman -S $pkg"
elif command -v zypper >/dev/null 2>&1; then
echo " sudo zypper install -y $pkg"
fi
}
for bin in curl jq; do
if ! command -v "$bin" >/dev/null 2>&1; then if ! command -v "$bin" >/dev/null 2>&1; then
echo "Required dependency '$bin' is not installed." >&2 echo "Required dependency '$bin' is not installed. Try:" >&2
suggest_package_install "$bin" >&2
exit 1 exit 1
fi fi
done done
if ! command -v systemctl >/dev/null 2>&1; then
echo "systemctl was not found — this installer requires a systemd-based Linux distribution." >&2
exit 1
fi
echo "Installing Schedule Task Manager agent from $API_URL ..." echo "Installing Schedule Task Manager agent from $API_URL ..."
curl -fsSL "$API_URL/agent/linux/report-tasks.sh" -o "$INSTALL_DIR/schedule-task-manager-agent.sh" curl -fsSL "$API_URL/agent/linux/report-tasks.sh" -o "$INSTALL_DIR/schedule-task-manager-agent.sh"
@@ -69,4 +95,4 @@ echo "Installed. Running an initial report now..."
"$INSTALL_DIR/schedule-task-manager-agent.sh" "$INSTALL_DIR/schedule-task-manager-agent.sh"
echo "Done. The agent reports every ${INTERVAL_MINUTES} minute(s) via the 'schedule-task-manager-agent.timer' systemd timer." echo "Done. The agent reports every ${INTERVAL_MINUTES} minute(s) via the 'schedule-task-manager-agent.timer' systemd timer."
echo "To remove it later: curl -fsSL $API_URL/agent/linux/uninstall.sh | bash" echo "To remove it later: curl -fsSL $API_URL/agent/linux/uninstall.sh | sudo bash"
+19 -1
View File
@@ -23,9 +23,27 @@ if [[ -z "$API_URL" || -z "$API_TOKEN" ]]; then
exit 1 exit 1
fi fi
suggest_package_install() {
local pkg="$1"
if command -v apt-get >/dev/null 2>&1; then
echo " sudo apt-get update && sudo apt-get install -y $pkg"
elif command -v dnf >/dev/null 2>&1; then
echo " sudo dnf install -y $pkg"
elif command -v yum >/dev/null 2>&1; then
echo " sudo yum install -y $pkg"
elif command -v apk >/dev/null 2>&1; then
echo " sudo apk add $pkg"
elif command -v pacman >/dev/null 2>&1; then
echo " sudo pacman -S $pkg"
elif command -v zypper >/dev/null 2>&1; then
echo " sudo zypper install -y $pkg"
fi
}
for bin in curl jq; do for bin in curl jq; do
if ! command -v "$bin" >/dev/null 2>&1; then if ! command -v "$bin" >/dev/null 2>&1; then
echo "Required dependency '$bin' is not installed." >&2 echo "Required dependency '$bin' is not installed. Try:" >&2
suggest_package_install "$bin" >&2
exit 1 exit 1
fi fi
done done
+5 -2
View File
@@ -2,8 +2,9 @@
# Removes the Schedule Task Manager agent from this server: stops and deletes # Removes the Schedule Task Manager agent from this server: stops and deletes
# its systemd timer/service, the installed script, and its credentials file. # its systemd timer/service, the installed script, and its credentials file.
# #
# Usage: # Usage (must run as root — if not already root, put sudo right after the
# curl -fsSL https://schedule.example.lan/agent/linux/uninstall.sh | bash # pipe so it applies to bash, not to curl):
# curl -fsSL https://schedule.example.lan/agent/linux/uninstall.sh | sudo bash
# #
set -euo pipefail set -euo pipefail
@@ -13,6 +14,8 @@ SYSTEMD_DIR="/etc/systemd/system"
if [[ "$EUID" -ne 0 ]]; then if [[ "$EUID" -ne 0 ]]; then
echo "This uninstaller must be run as root." >&2 echo "This uninstaller must be run as root." >&2
echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2
echo " curl -fsSL ... | sudo bash" >&2
exit 1 exit 1
fi fi
+4 -4
View File
@@ -89,8 +89,8 @@ export default function ServersAdmin() {
<h3>Agent token for {newToken.server.name}</h3> <h3>Agent token for {newToken.server.name}</h3>
<p>This token is only shown once — copy it now.</p> <p>This token is only shown once — copy it now.</p>
<code className="token-value">{newToken.token}</code> <code className="token-value">{newToken.token}</code>
<p>Install the agent on the server:</p> <p>Install the agent on the server (run as root — put sudo right after the pipe, not before curl):</p>
<pre className="install-command">{`curl -fsSL ${window.location.origin}/agent/linux/install.sh | API_URL=${window.location.origin} API_TOKEN=${newToken.token} bash`}</pre> <pre className="install-command">{`curl -fsSL ${window.location.origin}/agent/linux/install.sh | sudo API_URL=${window.location.origin} API_TOKEN=${newToken.token} bash`}</pre>
<button onClick={() => setNewToken(null)} className="btn"> <button onClick={() => setNewToken(null)} className="btn">
Dismiss Dismiss
</button> </button>
@@ -100,8 +100,8 @@ export default function ServersAdmin() {
{uninstallFor && ( {uninstallFor && (
<section className="token-reveal uninstall-reveal"> <section className="token-reveal uninstall-reveal">
<h3>Uninstall agent from {uninstallFor.name}</h3> <h3>Uninstall agent from {uninstallFor.name}</h3>
<p>Run this on the server to stop and remove the agent (its entry here is kept):</p> <p>Run this on the server as root to stop and remove the agent (its entry here is kept):</p>
<pre className="install-command">{`curl -fsSL ${window.location.origin}/agent/linux/uninstall.sh | bash`}</pre> <pre className="install-command">{`curl -fsSL ${window.location.origin}/agent/linux/uninstall.sh | sudo bash`}</pre>
<button onClick={() => setUninstallFor(null)} className="btn"> <button onClick={() => setUninstallFor(null)} className="btn">
Dismiss Dismiss
</button> </button>