memory_service.py 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. """Render (never install) a Linux systemd memory-isolated API service."""
  2. import argparse
  3. import hashlib
  4. from pathlib import Path
  5. import re
  6. import sys
  7. def unit_name(root):
  8. return 'business-assistant-' + hashlib.sha256(str(root).encode()).hexdigest()[:12] + '.service'
  9. def render(root, user, memory_mb, *, offline=False):
  10. root = str(root)
  11. # Keep the generated systemd command unambiguous; no shell evaluation or
  12. # systemd specifiers from user-controlled paths.
  13. if not re.fullmatch(r'/[A-Za-z0-9_./-]+', root) or '..' in root.split('/'):
  14. raise ValueError('service_path_requires_simple_absolute_linux_path')
  15. if not re.fullmatch(r'[a-z_][a-z0-9_-]{0,31}', user) or user == 'root':
  16. raise ValueError('service_requires_non_root_user')
  17. if type(memory_mb) is not int or not 2048 <= memory_mb <= 1048576:
  18. raise ValueError('invalid_memory_budget')
  19. soft = memory_mb * 3 // 4
  20. deploy_script = 'deploy-offline.sh' if offline else 'deploy.sh'
  21. return f'''[Unit]
  22. Description=Business Assistant memory-isolated API
  23. After=network-online.target
  24. Wants=network-online.target
  25. StartLimitIntervalSec=300
  26. StartLimitBurst=3
  27. [Service]
  28. Type=simple
  29. User={user}
  30. WorkingDirectory={root}
  31. ExecStart=/bin/sh {root}/{deploy_script} serve
  32. Environment=API_REQUIRE_MEMORY_CGROUP=1
  33. Environment=API_MEMORY_HARD_LIMIT_MB={memory_mb}
  34. Environment=API_MEMORY_BUDGET_MB={soft}
  35. MemoryAccounting=yes
  36. MemoryHigh={memory_mb * 7 // 8}M
  37. MemoryMax={memory_mb}M
  38. MemorySwapMax=0
  39. OOMPolicy=kill
  40. KillMode=control-group
  41. Restart=on-failure
  42. RestartSec=10
  43. TimeoutStopSec=45
  44. NoNewPrivileges=yes
  45. [Install]
  46. WantedBy=multi-user.target
  47. '''
  48. def main():
  49. parser = argparse.ArgumentParser(description=__doc__)
  50. parser.add_argument('--user', required=True)
  51. parser.add_argument('--memory-max-mb', type=int, required=True)
  52. parser.add_argument('--offline', action='store_true')
  53. args = parser.parse_args()
  54. root = Path(__file__).resolve().parents[1]
  55. content = render(root, args.user, args.memory_max_mb, offline=args.offline)
  56. output = root / 'output/deploy' / unit_name(root)
  57. output.parent.mkdir(parents=True, exist_ok=True)
  58. output.write_text(content, encoding='utf-8')
  59. print(output)
  60. if __name__ == '__main__':
  61. main()