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.
 
 
 

162 lines
4.9 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import sys
  18. import json
  19. import time
  20. import logging
  21. import asyncio
  22. import datetime
  23. import threading
  24. from collections import defaultdict
  25. CATSOOP_LOC = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
  26. if CATSOOP_LOC not in sys.path:
  27. sys.path.append(CATSOOP_LOC)
  28. from catsoop.cslog import unprep
  29. import catsoop.base_context as base_context
  30. import websockets
  31. DEBUG = True
  32. CHECKER_DB_LOC = os.path.join(base_context.cs_data_root, "_logs", "_checker")
  33. RUNNING = os.path.join(CHECKER_DB_LOC, "running")
  34. QUEUED = os.path.join(CHECKER_DB_LOC, "queued")
  35. RESULTS = os.path.join(CHECKER_DB_LOC, "results")
  36. CURRENT = {"queued": [], "running": set()}
  37. PORTNUM = base_context.cs_checker_server_port
  38. logging.basicConfig(format="%(asctime)s - %(message)s")
  39. LOGGER = logging.getLogger("cs")
  40. WSLOGGER = logging.getLogger("websockets.server")
  41. WSLOGGER.setLevel(LOGGER.level)
  42. WSLOGGER.addHandler(logging.StreamHandler())
  43. def log(msg):
  44. dt = datetime.datetime.now()
  45. omsg = "[reporter:%s]: %s" % (dt, msg)
  46. LOGGER.info(omsg)
  47. def get_status(magic):
  48. try:
  49. s = CURRENT["queued"].index(magic) + 1
  50. except:
  51. if magic in CURRENT["running"]:
  52. s = "running"
  53. elif os.path.isfile(os.path.join(RESULTS, magic[0], magic[1], magic)):
  54. s = "results"
  55. else:
  56. return
  57. return s
  58. async def reporter(websocket, path):
  59. DEBUG = True
  60. if DEBUG:
  61. LOGGER.error("Waiting for websocket recv")
  62. magic_json = await websocket.recv()
  63. magic = json.loads(magic_json)["magic"]
  64. if DEBUG:
  65. log("Got message magic=%s, json=%s" % (magic, magic_json))
  66. last_ping = time.time()
  67. last_status = None
  68. while True:
  69. if DEBUG:
  70. log("In main loop")
  71. t = time.time()
  72. # if it's been more than 10 seconds since we've pinged, ping again.
  73. if t - last_ping > 10:
  74. try:
  75. await asyncio.wait_for(websocket.ping(), timeout=10)
  76. last_ping = time.time()
  77. except asyncio.TimeoutError:
  78. # no response from ping in 10 seconds. quit.
  79. break
  80. # get our current status
  81. status = None
  82. try:
  83. status = CURRENT["queued"].index(magic) + 1
  84. except:
  85. if magic in CURRENT["running"]:
  86. status = "running"
  87. elif os.path.isfile(os.path.join(RESULTS, magic[0], magic[1], magic)):
  88. status = "results"
  89. # if our status hasn't changed, or if we don't know yet, don't send
  90. # anything; just keep waiting.
  91. if status is None or status == last_status:
  92. await asyncio.sleep(0.3)
  93. continue
  94. # otherwise, we should send a message.
  95. if isinstance(status, int):
  96. msg = {"type": "inqueue", "position": status}
  97. elif status == "running":
  98. try:
  99. start = os.stat(os.path.join(RUNNING, magic)).st_ctime
  100. except:
  101. start = time.time()
  102. msg = {"type": "running", "started": start, "now": time.time()}
  103. elif status == "results":
  104. try:
  105. with open(os.path.join(RESULTS, magic[0], magic[1], magic), "rb") as f:
  106. m = unprep(f.read())
  107. except:
  108. return
  109. sb = m.get("score_box", "?")
  110. r = m.get("response", "?")
  111. msg = {"type": "newresult", "score_box": sb, "response": r}
  112. else:
  113. msg = None
  114. if msg is not None:
  115. await websocket.send(json.dumps(msg))
  116. if status == "results":
  117. break
  118. last_status = status
  119. await asyncio.sleep(0.3)
  120. def updater():
  121. CURRENT["queued"] = [i.split("_")[1] for i in sorted(os.listdir(QUEUED))]
  122. CURRENT["running"] = {i.name for i in os.scandir(RUNNING)}
  123. crun = CURRENT["running"]
  124. if DEBUG and crun:
  125. log("updater queued=%s" % crun)
  126. loop.call_later(0.3, updater)
  127. log("Starting reporter on port=%s" % PORTNUM)
  128. start_server = websockets.serve(reporter, "0.0.0.0", PORTNUM)
  129. loop = asyncio.get_event_loop()
  130. log("Running start_server")
  131. loop.run_until_complete(start_server)
  132. loop.call_soon(updater)
  133. loop.run_forever()
  134. log("Reporter exiting")