CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

215 lines
6.9 KiB

  1. #!/usr/bin/env python3
  2. # This file is part of CAT-SOOP
  3. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  4. #
  5. # This program is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU Affero General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option) any
  8. # later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import os
  18. import sys
  19. import time
  20. import atexit
  21. import signal
  22. import getpass
  23. import logging
  24. import hashlib
  25. import sqlite3
  26. import subprocess
  27. from datetime import datetime
  28. LOGGER = logging.getLogger("cs")
  29. os.setpgrp()
  30. scripts_dir = os.path.abspath(os.path.dirname(__file__))
  31. base_dir = os.path.abspath(os.path.join(scripts_dir, ".."))
  32. cs_logo = r"""
  33. \
  34. / /\__/\
  35. \__=( o_O )=
  36. (__________)
  37. |_ |_ |_ |_
  38. CAT-SOOP
  39. """
  40. def main():
  41. import catsoop.base_context as base_context
  42. import catsoop.loader as loader
  43. from catsoop.process import set_pdeathsig
  44. # Make sure the checker database is set up
  45. checker_db_loc = os.path.join(base_context.cs_data_root, "_logs", "_checker")
  46. for subdir in ("queued", "running", "results", "staging"):
  47. os.makedirs(os.path.join(checker_db_loc, subdir), exist_ok=True)
  48. procs = [
  49. (scripts_dir, [sys.executable, "checker.py"], 0.1, "Checker"),
  50. (scripts_dir, [sys.executable, "reporter.py"], 0.1, "Reporter"),
  51. ]
  52. # put plugin autostart scripts into the list
  53. ctx = loader.generate_context([])
  54. for plugin in loader.available_plugins(ctx, course=None):
  55. script_dir = os.path.join(plugin, "autostart")
  56. if os.path.isdir(script_dir):
  57. for script in sorted(os.listdir(script_dir)):
  58. if not script.endswith(".py"):
  59. continue
  60. procs.append(
  61. (
  62. script_dir,
  63. [sys.executable, script],
  64. 0.1,
  65. os.path.join(script_dir, script),
  66. )
  67. )
  68. # set up WSGI options
  69. if base_context.cs_wsgi_server == "cheroot":
  70. print("[start_catsoop] Using cheroot for web service")
  71. wsgi_ports = base_context.cs_wsgi_server_port
  72. if not isinstance(wsgi_ports, list):
  73. wsgi_ports = [wsgi_ports]
  74. for port in wsgi_ports:
  75. procs.append(
  76. (
  77. scripts_dir,
  78. [sys.executable, "wsgi_server.py", str(port)],
  79. 0.1,
  80. "WSGI Server at Port %d" % port,
  81. )
  82. )
  83. elif base_context.cs_wsgi_server == "uwsgi":
  84. print("[start_catsoop] Using uwsgi for web service")
  85. if (
  86. base_context.cs_wsgi_server_min_processes
  87. >= base_context.cs_wsgi_server_max_processes
  88. ):
  89. uwsgi_opts = ["--processes", str(base_context.cs_wsgi_server_min_processes)]
  90. else:
  91. uwsgi_opts = [
  92. "--cheaper",
  93. str(base_context.cs_wsgi_server_min_processes),
  94. "--workers",
  95. str(base_context.cs_wsgi_server_max_processes),
  96. "--cheaper-step",
  97. "1",
  98. "--cheaper-initial",
  99. str(base_context.cs_wsgi_server_min_processes),
  100. ]
  101. uwsgi_opts = [
  102. "--http",
  103. ":%s" % base_context.cs_wsgi_server_port,
  104. "--thunder-lock",
  105. "--wsgi-file",
  106. "wsgi.py",
  107. "--touch-reload",
  108. "wsgi.py",
  109. ] + uwsgi_opts
  110. procs.append((base_dir, ["uwsgi"] + uwsgi_opts, 0.1, "WSGI Server"))
  111. else:
  112. raise ValueError("unsupported wsgi server: %r" % base_context.cs_wsgi_server)
  113. running = []
  114. for (ix, (wd, cmd, slp, name)) in enumerate(procs):
  115. LOGGER.error("[start_catsoop] Starting %s (cmd=%s, wd=%s)" % (name, cmd, wd))
  116. running.append(
  117. subprocess.Popen(cmd, cwd=wd, preexec_fn=set_pdeathsig(signal.SIGTERM))
  118. )
  119. LOGGER.error("[start_catsoop] %s has pid=%s" % (name, running[-1].pid))
  120. time.sleep(slp)
  121. def _kill_children():
  122. for ix, i in enumerate(running):
  123. os.kill(i.pid, signal.SIGTERM)
  124. atexit.register(_kill_children)
  125. while True:
  126. for idx, (procinfo, proc) in enumerate(
  127. zip(procs, running)
  128. ): # restart running process if it has died
  129. if proc.poll() is not None:
  130. (wd, cmd, slp, name) = procinfo
  131. LOGGER.error(
  132. "[start_catsoop] %s (pid=%s) was killed, restarting it"
  133. % (name, proc.pid)
  134. )
  135. running[idx] = subprocess.Popen(
  136. cmd, cwd=wd, preexec_fn=set_pdeathsig(signal.SIGTERM)
  137. )
  138. LOGGER.error(
  139. "[start_catsoop] Starting %s (cmd=%s, wd=%s) as pid=%s"
  140. % (name, cmd, wd, running[idx].pid)
  141. )
  142. time.sleep(1)
  143. def startup_catsoop(config_loc=None):
  144. print(cs_logo)
  145. print("Using base_dir=%s" % base_dir)
  146. config_loc = config_loc or os.environ.get(
  147. "CATSOOP_CONFIG", os.path.join(base_dir, "catsoop", "config.py")
  148. )
  149. if not os.path.isfile(config_loc):
  150. print(
  151. "%s does not exist. Please configure CAT-SOOP first, either by editing that file manually, or by running setup_catsoop.py"
  152. % config_loc
  153. )
  154. sys.exit(1)
  155. print("Using catsoop configuration specified by %s" % config_loc)
  156. os.environ["CATSOOP_CONFIG"] = config_loc
  157. if base_dir not in sys.path:
  158. sys.path.append(base_dir)
  159. _enc_salt_file = os.path.join(os.path.dirname(config_loc), "encryption_salt")
  160. _enc_hash_file = os.path.join(
  161. os.path.dirname(config_loc), "encryption_passphrase_hash"
  162. )
  163. if os.path.isfile(_enc_salt_file):
  164. with open(_enc_salt_file, "rb") as f:
  165. salt = f.read()
  166. with open(_enc_hash_file, "rb") as f:
  167. phash = f.read()
  168. print(
  169. "CAT-SOOP's logs are encrypted. Please enter the encryption passphrase below."
  170. )
  171. while True:
  172. pphrase = getpass.getpass("Encryption passphrase: ")
  173. h = hashlib.pbkdf2_hmac("sha512", pphrase.encode("utf8"), salt, 100000)
  174. if h == phash:
  175. os.environ["CATSOOP_PASSPHRASE"] = pphrase
  176. break
  177. else:
  178. print("Passphrase does not match stored hash. Try again.")
  179. main()
  180. if __name__ == "__main__":
  181. startup_catsoop()