Skip to main content

Cheatsheet and Quiz

Learning Focus

Use this page as a quick reference after you've completed the full documentation. The cheat sheet is designed for copy-paste during daily operations. The quiz tests your understanding of key concepts.


Cheat Sheet

Complete systemd.path Cheat Sheet — Click to Expand
systemd-path-cheat-sheet.txt
# ══════════════════════════════════════════════════════════════════════════════
# systemd.path — Quick Reference Cheat Sheet
# ══════════════════════════════════════════════════════════════════════════════


# ── STEP 1: Create the Path Unit ──────────────────────────────────────────────
# File: /etc/systemd/system/mytask.path

[Unit]
Description=Watch for incoming files

[Path]
DirectoryNotEmpty=/var/www/incoming # See directive reference below
MakeDirectory=yes # Create the directory if it doesn't exist
DirectoryMode=0775 # Permissions for auto-created dirs
Unit=mytask.service # Optional: override target service name

[Install]
WantedBy=paths.target


# ── STEP 2: Create the Service Unit ───────────────────────────────────────────
# File: /etc/systemd/system/mytask.service

[Unit]
Description=Process incoming files
StartLimitBurst=10 # Rate limit: max starts in window
StartLimitIntervalSec=60 # Rate limit: window size

[Service]
Type=oneshot
User=www-data
Group=www-data
ExecStart=/usr/local/bin/process.sh # MUST be absolute path
RuntimeMaxSec=30m # Kill after 30 minutes
StandardOutput=append:/var/log/mytask.log
StandardError=append:/var/log/mytask.log
NoNewPrivileges=true # Basic security hardening
PrivateTmp=true
# No [Install] section — activated by the .path unit


# ── STEP 3: Activate ─────────────────────────────────────────────────────────
sudo systemctl daemon-reload
sudo systemctl enable --now mytask.path


# ── Watch Directive Reference ─────────────────────────────────────────────────
PathExists=PATH # Triggers when path exists (LOOPS if not deleted)
PathExistsGlob=PATTERN # Triggers on glob match (LOOPS if matches remain)
PathChanged=PATH # Triggers after file is written AND fd is closed
PathModified=PATH # Triggers immediately on ANY write
DirectoryNotEmpty=PATH # Triggers if directory is non-empty (LOOPS until empty)


# ── Management Commands ──────────────────────────────────────────────────────
sudo systemctl daemon-reload # Re-read unit files (REQUIRED after edits)
sudo systemctl enable --now mytask.path # Enable on boot + start now
sudo systemctl disable --now mytask.path # Disable + stop
sudo systemctl start mytask.path # Start the watcher
sudo systemctl stop mytask.path # Stop the watcher
sudo systemctl restart mytask.path # Restart the watcher
systemctl status mytask.path # Check watch state
systemctl status mytask.service # Check service result


# ── Log Commands ──────────────────────────────────────────────────────────────
journalctl -u mytask.service -f # Follow live output
journalctl -u mytask.service -n 50 # Last 50 lines
journalctl -u mytask.service --since today # Today's logs
journalctl -u mytask.service -p err # Errors only
journalctl -u mytask.path -u mytask.service # Combined path+service logs


# ── Debug Commands ────────────────────────────────────────────────────────────
sudo systemd-analyze verify mytask.path # Validate syntax
sudo systemd-analyze verify mytask.service # Validate syntax
systemctl cat mytask.path # Show unit file content
systemctl show -p Triggers mytask.path # What service does it trigger?
systemctl list-units --type=path --all # List all path units
sudo systemctl reset-failed # Clear failed states
systemd-analyze security mytask.service # Security audit


# ── User Units (no root) ─────────────────────────────────────────────────────
# Files go in: ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now mytask.path
systemctl --user status mytask.path
journalctl --user -u mytask.service -f
sudo loginctl enable-linger "$USER" # Keep running after logout


# ── inotify Watch Limits ─────────────────────────────────────────────────────
cat /proc/sys/fs/inotify/max_user_watches # Check current limit
echo 65536 | sudo tee /proc/sys/fs/inotify/max_user_watches # Temporary increase
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.conf # Permanent
sudo sysctl -p


# ── Full Cleanup ──────────────────────────────────────────────────────────────
sudo systemctl disable --now mytask.path
sudo rm /etc/systemd/system/mytask.path /etc/systemd/system/mytask.service
sudo systemctl daemon-reload
sudo systemctl reset-failed

Directive Quick Reference

DirectiveFires WhenPre-existing?Re-triggers?Loop RiskScript Must...
PathExists=Path existsYesYesHighDelete the file
PathExistsGlob=Glob match existsYesYesHighDelete/move all matches
PathChanged=File changed + fd closedNoNoLowNothing
PathModified=Any writeNoNoLowNothing
DirectoryNotEmpty=Dir is non-emptyYesYesMediumDelete/move files

Decision Quick Reference

I need to...Use this directive
Process files in a drop folderDirectoryNotEmpty=
React to a signal file (touch)PathExists=
React to a file being uploaded (SFTP)PathChanged=
React instantly to any file writePathModified=
Process only specific file typesPathExistsGlob=
Reload a service when config changesPathChanged= on the config dir
Alert on critical file modificationPathModified=
Wait for a file to appear before starting somethingPathExists=

Hands-On Lab

Lab 1 — Basic Drop Folder (5 minutes)

  1. Create /etc/systemd/system/lab-drop.path with DirectoryNotEmpty=/tmp/lab-drop and MakeDirectory=yes.
  2. Create /etc/systemd/system/lab-drop.service with Type=oneshot and ExecStart=/bin/sh -c 'echo "Files:" && ls /tmp/lab-drop && rm -f /tmp/lab-drop/*'.
  3. Run sudo systemctl daemon-reload && sudo systemctl enable --now lab-drop.path.
  4. Open a second terminal: journalctl -f -u lab-drop.service.
  5. Drop files: touch /tmp/lab-drop/a.txt /tmp/lab-drop/b.txt.
  6. Watch the journal show the files being listed and removed.
  7. Verify: ls /tmp/lab-drop should be empty.

Lab 2 — Signal File Pattern (5 minutes)

  1. Create /etc/systemd/system/lab-signal.path with PathExists=/tmp/lab-signal.txt.
  2. Create /etc/systemd/system/lab-signal.service with ExecStart=/bin/sh -c 'echo "Signal received at $(date)" && rm /tmp/lab-signal.txt'.
  3. Reload and enable: sudo systemctl daemon-reload && sudo systemctl enable --now lab-signal.path.
  4. Trigger: touch /tmp/lab-signal.txt.
  5. Check: journalctl -u lab-signal.service -n 5 --no-pager.
  6. Verify: /tmp/lab-signal.txt should not exist.

Lab 3 — Config Change Watcher (5 minutes)

  1. Create /etc/systemd/system/lab-config.path with PathChanged=/tmp/lab-config.conf.
  2. Create /etc/systemd/system/lab-config.service with ExecStart=/bin/sh -c 'echo "Config changed at $(date): $(cat /tmp/lab-config.conf)"'.
  3. Mark the config file as existing: echo "original" > /tmp/lab-config.conf.
  4. Reload and enable: sudo systemctl daemon-reload && sudo systemctl enable --now lab-config.path.
  5. Modify the config: echo "updated value" > /tmp/lab-config.conf.
  6. Check: journalctl -u lab-config.service -n 5 --no-pager.
  7. Verify: The log shows "Config changed" with the new value.

Lab 4 — Debugging Practice (5 minutes)

  1. Create a path unit with a deliberate typo — use Directorynotempty= (wrong case) instead of DirectoryNotEmpty=.
  2. Run sudo systemd-analyze verify /etc/systemd/system/lab-broken.path — observe the error.
  3. Fix the typo and run verify again — observe no output (success).
  4. Create a service unit that references a script that doesn't exist.
  5. Enable the path unit and trigger it.
  6. Use journalctl -u lab-broken.service -n 10 to see the error.
  7. Fix the script path and test again.

Lab 5 — Cleanup

cleanup-labs.sh
for unit in lab-drop lab-signal lab-config lab-broken; do
sudo systemctl disable --now "${unit}.path" 2>/dev/null || true
sudo rm -f "/etc/systemd/system/${unit}.path" "/etc/systemd/system/${unit}.service"
done
sudo systemctl daemon-reload
rm -f /tmp/lab-signal.txt /tmp/lab-config.conf
rm -rf /tmp/lab-drop

Build Task — Full Cache-Clear Signal System

Write the complete units and the accompanied trigger script to allow clearing the WordPress object cache by an SFTP client (without SSH), with full logging and security hardening.

Solution
/etc/systemd/system/clear-cache.path
[Unit]
Description=Watch for WP cache flush signal

[Path]
PathExists=/var/www/html/clear_cache.txt

[Install]
WantedBy=paths.target
/etc/systemd/system/clear-cache.service
[Unit]
Description=Flush WordPress cache on signal
StartLimitBurst=5
StartLimitIntervalSec=60

[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/flush-wp-cache.sh
RuntimeMaxSec=2m
StandardOutput=append:/var/log/cache-flush.log
StandardError=append:/var/log/cache-flush.log
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log /var/www/html
/usr/local/bin/flush-wp-cache.sh
#!/usr/bin/env bash
set -euo pipefail
echo "[$(date -Is)] Cache flush triggered"
/usr/local/bin/wp cache flush --path=/var/www/html
rm /var/www/html/clear_cache.txt
echo "[$(date -Is)] Done"
install.sh
sudo chmod +x /usr/local/bin/flush-wp-cache.sh
sudo systemctl daemon-reload
sudo systemctl enable --now clear-cache.path

# Test:
touch /var/www/html/clear_cache.txt
sleep 1
journalctl -u clear-cache.service -n 10

Mini Quiz

Test your understanding of systemd.path. Try answering before checking the solutions.

Questions

  1. What is the difference between PathChanged= and PathModified=, and when does each fire?

  2. Which unit do you systemctl enable — the .path or the .service?

  3. What happens if you use PathExists= and the service never deletes the trigger file?

  4. What directive would you use to process a "drop folder" where files are continuously being added?

  5. Why is Type=oneshot the recommended service type for path-triggered services?

  6. A colleague added a new path unit but it isn't triggering. What is the first command they likely forgot to run?

  7. Can you watch multiple files or directories in a single .path unit? If yes, is the logic AND or OR?

  8. How do you restrict a path-triggered service to run no more than 3 times per minute?

  9. What journalctl command shows live output from a triggered service?

  10. Your path unit is active (waiting) but a pre-existing file is not triggering PathChanged=. Why, and what directive would have fired immediately instead?


Answers
  1. PathChanged= fires after the file is written and the file descriptor is closed (safe for uploads). PathModified= fires immediately on any write, even partial writes (unsafe for uploads, good for instant alerts).

  2. The .path unit. systemctl enable --now mytask.path. Enabling the .service directly would run it once at boot — not on filesystem events.

  3. Infinite loop. The path unit detects the file still exists after the service exits, so it starts the service again, forever. The journal fills up and CPU spikes.

  4. DirectoryNotEmpty=. It triggers when the directory contains at least one file and re-triggers after the service processes and removes a file — if more files remain.

  5. Type=oneshot tells systemd the process runs once and exits. systemd won't start a second instance while the first is running (concurrency safety). Path-triggered jobs are inherently "run once, exit" — not long-running daemons.

  6. sudo systemctl daemon-reload. Without this, systemd is still using the old (or non-existent) unit file from before the edit.

  7. Yes, you can use multiple watch directives. The logic is OR — the service triggers when any directive matches.

  8. Add to the [Unit] section of the .service file:

    StartLimitBurst=3
    StartLimitIntervalSec=60
  9. journalctl -u mytask.service -f — the -f flag follows live output like tail -f.

  10. PathChanged= only fires on future changes, not pre-existing files. Use PathExists= if you need the service to trigger immediately for a file that already exists when the path unit starts.


What's Next

You've completed the full systemd.path documentation. Here are related topics to explore: