python-daemon-pattern.md
4.7 KB
Python Daemon Pattern for VisualAgent
Session: 2026-06-27 — Python daemon replaces shell daemon, eliminates all shell-origin bugs
Why Python Instead of Shell
Shell-based VisualAgent daemons (visualagent_daemon.sh, visual_agent_service.sh) suffer from multiple failure modes that compound over time:
| Problem | Shell Cause | Python Fix |
|---|---|---|
python3 not in root $PATH |
Shell inherits PATH; root context loses Termux paths | Full path in shebang: #!/data/data/com.termux/files/usr/bin/python3 |
JSON quoting breaks in su -c "echo '...'" |
Nested single/double quotes explode | json.load() / json.dump() — no quoting needed |
grep parsing fragile on Android toybox |
`grep -o '"type"...' | cut -d'"' -f4` misses edge cases |
Daemon dies when su session exits |
su -c 'nohup ... &' then su exits → SIGHUP |
Started via Hermes terminal(background=true) with su -c 'exec ...' |
| Code version mismatch | Shell reads script line-by-line into memory; edits to file don't affect running process | Same issue exists, but Python's single-file design and clear versioning (v1, v2, v3) makes it easier to track |
| Race condition: multiple instances write same result file | Each cron invocation starts a new shell daemon | Single Python process with os.rename() (atomic on same fs) |
The Correct Startup Pattern
The ONLY reliable way to start a long-lived daemon on Android from Hermes:
# In Hermes:
terminal(background=true, command='su -c "exec /data/data/com.termux/files/usr/bin/python3 /data/data/com.termux/files/home/va_daemon.py"')
Why this works:
terminal(background=true)— Hermes keeps a persistent session alive as the daemon's parentsu -c 'exec ...'—execreplaces the su shell process with Python, so there's no intermediate shell to SIGHUP- Full Python path — avoids PATH resolution issues in root context
What does NOT work (and why):
| Attempt | Failure Mode |
|---|---|
su -c 'nohup ./daemon.sh &' |
su session exits → child killed |
su -c 'setsid ./daemon.sh &' |
Same — su exiting kills the session |
su -c './daemon.sh &' (foreground) |
Hermes rejects & in foreground mode |
bash daemon.sh start (no su) |
Daemon can't run screencap, input, monkey without root |
Cron Job Competing Instances
A cron job running */5 * * * * that spawns Hermes agents to "monitor VisualAgent" creates a cascading problem:
- Cron fires → new Hermes agent starts
- That agent starts a daemon (shell or Python)
- 5 minutes later, cron fires again → another Hermes agent + another daemon
- Multiple daemons now compete for the same command files
- Result files get concatenated JSON from both daemons
- Log fills with interleaved messages from both
Solution:
- If a persistent Python daemon is running, pause the cron job:
hermes cron pause <job_id> - The daemon handles monitoring; the cron job is redundant and harmful
- See pitfall #68 (below)
Deployment Checklist
When deploying a new VisualAgent daemon:
- Kill all old daemons:
su -c "ps -ef | grep -i visual | awk '{print \$2}' | xargs kill -9" - Disable old scripts:
mv visualagent_daemon.sh .bak_visualagent_daemon.sh(prevents accidental invocation) - Pause competing cron:
hermes cron pause <id>if a cron job also monitors commands - Deploy
va_daemon.py: Copy to device, ensure shebang points to correct Python - Start via Hermes background: Use the correct startup pattern above
- Verify with a test command:
echo '{"command":"screenshot"}' > /sdcard/VisualAgent/commands/test.json - Check result file:
cat /sdcard/VisualAgent/results/result_test_*.jsonshould show{"status": "success", ...}with spaces (Pythonjson.dumpformat, not compact shellechoformat)
Distinguishing Python vs Shell Daemon Output
| Signal | Python Daemon | Shell Daemon |
|---|---|---|
| JSON spacing | {"status": "success", ...} (spaces after colons) |
{"status":"success",...} (compact, no spaces) |
| Log format | done: filename.json (success) |
结果已保存: / 已处理: / 未知命令: (Chinese) |
| Error messages | English: unknown cmd: ..., json parse error: ... |
Chinese: 未知命令:, 无法解析命令类型 |
If you see BOTH formats in the same result file or log, there are TWO daemon instances running — kill the old one.
Related Pitfalls
- Pitfall #23: Daemon lifecycle and stale PID files
- Pitfall #39: Pre-existing watchers causing file disappearance
- Pitfall #68:
su -csessions kill child processes on exit - Pitfall #69: Daemon code version mismatch (process has old code in memory)