backend.workers.restart_4cat
Restart 4CAT and optionally upgrade it to the latest release
1""" 2Restart 4CAT and optionally upgrade it to the latest release 3""" 4import subprocess 5import requests 6import hashlib 7import oslex 8import json 9import time 10import uuid 11import sys 12 13from pathlib import Path 14 15from backend.lib.worker import BasicWorker 16from common.lib.exceptions import WorkerInterruptedException 17 18 19class FourcatRestarterAndUpgrader(BasicWorker): 20 """ 21 Restart 4CAT and optionally upgrade it to the latest release 22 23 Why implement this as a worker? Trying to have 4CAT restart itself leads 24 to an interesting conundrum: it will not be able to report the outcome of 25 the restart, because whatever bit of code is keeping track of that will be 26 interrupted by restarting 4CAT. 27 28 Using a worker has the benefit of it restarting after 4CAT restarts, so it 29 can then figure out that 4CAT was just restarted and report the outcome. It 30 then uses a log file to keep track of the results. The log file can then be 31 used by other parts of 4CAT to see if the restart was successful. 32 33 It does lead to another conundrum - what if due to some error, 4CAT never 34 restarts? Then this worker will not be run again to report its own failure. 35 There seem to be no clean ways around this, so anything watching the 36 outcome of the worker probably needs to implement some timeout after which 37 it is assumed that the restart/upgrade process failed catastrophically. 38 """ 39 type = "restart-4cat" 40 max_workers = 1 41 42 def work(self): 43 """ 44 Restart 4CAT and optionally upgrade it to the latest release 45 """ 46 # figure out if we're starting the restart or checking the result 47 # after 4cat has been restarted 48 is_resuming = self.job.data["attempts"] > 0 49 50 # prevent multiple restarts running at the same time which could blow 51 # up really fast 52 lock_file = Path(self.config.get("PATH_ROOT")).joinpath("config/restart.lock") 53 54 # this file has the log of the restart worker itself and is checked by 55 # the frontend to see how far we are 56 log_file_restart = Path(self.config.get("PATH_ROOT")).joinpath(self.config.get("PATH_LOGS")).joinpath("restart.log") 57 log_stream_restart = log_file_restart.open("a") 58 59 if not is_resuming: 60 log_stream_restart.write("Initiating 4CAT restart worker\n") 61 self.log.info("New restart initiated.") 62 63 # this lock file will ensure that people don't start two 64 # simultaneous upgrades or something 65 with lock_file.open("w") as outfile: 66 hasher = hashlib.blake2b() 67 hasher.update(str(uuid.uuid4()).encode("utf-8")) 68 outfile.write(hasher.hexdigest()) 69 70 # trigger a restart and/or upgrade 71 # returns a JSON with a 'status' key and a message, the message 72 # being the process output 73 74 if self.job.data["remote_id"].startswith("upgrade"): 75 command = sys.executable + " helper-scripts/migrate.py --repository %s --yes --restart" % \ 76 (oslex.quote(self.config.get("4cat.github_url"))) 77 if self.job.details and self.job.details.get("branch"): 78 # migrate to code in specific branch 79 command += f" --branch {oslex.quote(self.job.details['branch'])}" 80 else: 81 # migrate to latest release 82 command += " --release" 83 84 else: 85 command = sys.executable + " 4cat-daemon.py --no-version-check force-restart" 86 87 try: 88 # flush any writes before the other process starts writing to 89 # the stream 90 self.log.info(f"Running command {command}") 91 log_stream_restart.flush() 92 93 # the tricky part is that this command will interrupt the 94 # daemon, i.e. this worker! 95 # so we'll never get to actually send a response, if all goes 96 # well. but the file descriptor that stdout is piped to remains 97 # open, somehow, so we can use that to keep track of the output 98 # stdin needs to be /dev/null here because else when 4CAT 99 # restarts and we re-attempt to make a daemon, it will fail 100 # when trying to close the stdin file descriptor of the 101 # subprocess (man, that was a fun bug to hunt down) 102 process = subprocess.Popen(oslex.split(command), cwd=str(self.config.get("PATH_ROOT")), 103 stdout=log_stream_restart, stderr=log_stream_restart, 104 stdin=subprocess.DEVNULL) 105 106 while not self.interrupted: 107 # basically wait for either the process to quit or 4CAT to 108 # be restarted (hopefully the latter) 109 try: 110 # now see if the process is finished - if not a 111 # TimeoutExpired will be raised 112 process.wait(1) 113 break 114 except subprocess.TimeoutExpired: 115 pass 116 117 if process.returncode is not None: 118 # if we reach this, 4CAT was never restarted, and so the job failed 119 log_stream_restart.write( 120 f"\nUnexpected outcome of restart call ({process.returncode})\n") 121 122 raise RuntimeError() 123 else: 124 # interrupted before the process could finish (as it should) 125 self.log.info("Restart triggered. Restarting 4CAT.\n") 126 raise WorkerInterruptedException() 127 128 except (RuntimeError, subprocess.CalledProcessError) as e: 129 log_stream_restart.write(str(e)) 130 log_stream_restart.write( 131 "[Worker] Error while restarting 4CAT. The script returned a non-standard error code " 132 "(see above). You may need to restart 4CAT manually.\n") 133 self.log.error(f"Error restarting 4CAT. See {log_stream_restart.name} for details.") 134 lock_file.unlink() 135 self.job.finish() 136 137 finally: 138 log_stream_restart.close() 139 140 else: 141 # 4CAT back-end was restarted - now check the results and make the 142 # front-end restart or upgrade too 143 self.log.info("Restart worker resumed after restarting 4CAT, restart successful.") 144 log_stream_restart.write("4CAT restarted.\n") 145 with Path(self.config.get("PATH_ROOT")).joinpath("config/.current-version").open() as infile: 146 log_stream_restart.write(f"4CAT is now running version {infile.readline().strip()}.\n") 147 148 # we're gonna use some specific Flask routes to trigger this, i.e. 149 # we're interacting with the front-end through HTTP 150 api_host = "https://" if self.config.get("flask.https") else "http://" 151 if self.config.get("USING_DOCKER"): 152 import os 153 docker_exposed_port = os.environ['PUBLIC_PORT'] 154 api_host += f"host.docker.internal{':' + docker_exposed_port if docker_exposed_port != '80' else ''}" 155 else: 156 api_host += self.config.get("flask.server_name") 157 158 if self.job.data["remote_id"].startswith("upgrade") and self.config.get("USING_DOCKER"): 159 # when using Docker, the front-end needs to update separately 160 log_stream_restart.write("Telling front-end Docker container to upgrade...\n") 161 log_stream_restart.close() # close, because front-end will be writing to it 162 upgrade_ok = False 163 upgrade_timeout = False 164 try: 165 upgrade_url = api_host + "/admin/trigger-frontend-upgrade/" 166 with lock_file.open() as infile: 167 frontend_upgrade = requests.post(upgrade_url, data={"token": infile.read()}, timeout=(10 * 60)) 168 upgrade_ok = frontend_upgrade.json()["status"] == "OK" 169 except requests.RequestException: 170 pass 171 except TimeoutError: 172 upgrade_timeout = True 173 174 log_stream_restart = log_file_restart.open("a") 175 if not upgrade_ok: 176 if upgrade_timeout: 177 log_stream_restart.write("Upgrade timed out.") 178 log_stream_restart.write("Error upgrading front-end container. You may need to upgrade and restart" 179 "containers manually.\n") 180 lock_file.unlink() 181 return self.job.finish() 182 183 # restart front-end 184 log_stream_restart.write("Asking front-end to restart itself...\n") 185 log_stream_restart.flush() 186 try: 187 restart_url = api_host + "/admin/trigger-frontend-restart/" 188 with lock_file.open() as infile: 189 response = requests.post(restart_url, data={"token": infile.read()}, timeout=5).json() 190 191 if response.get("message"): 192 log_stream_restart.write(response.get("message") + "\n") 193 except (json.JSONDecodeError, requests.RequestException): 194 # this may happen because the server restarts and interrupts 195 # the request 196 pass 197 198 # wait for front-end to come online after a restart 199 time.sleep(3) # give some time for the restart to trigger 200 start_time = time.time() 201 frontend_ok = False 202 while time.time() < start_time + 60: 203 try: 204 frontend = requests.get(api_host + "/", timeout=5) 205 if frontend.status_code > 401: 206 time.sleep(2) 207 continue 208 frontend_ok = True 209 break 210 except requests.RequestException: 211 time.sleep(1) 212 continue 213 214 # too bad 215 if not frontend_ok: 216 log_stream_restart.write("Timed out waiting for front-end to restart. You may need to restart it " 217 "manually.\n") 218 self.log.error("Front-end did not come back online after restart") 219 else: 220 log_stream_restart.write("Front-end is available. Restart complete.") 221 self.log.info("Front-end is available. Restart complete.") 222 223 log_stream_restart.close() 224 lock_file.unlink() 225 226 self.job.finish()
20class FourcatRestarterAndUpgrader(BasicWorker): 21 """ 22 Restart 4CAT and optionally upgrade it to the latest release 23 24 Why implement this as a worker? Trying to have 4CAT restart itself leads 25 to an interesting conundrum: it will not be able to report the outcome of 26 the restart, because whatever bit of code is keeping track of that will be 27 interrupted by restarting 4CAT. 28 29 Using a worker has the benefit of it restarting after 4CAT restarts, so it 30 can then figure out that 4CAT was just restarted and report the outcome. It 31 then uses a log file to keep track of the results. The log file can then be 32 used by other parts of 4CAT to see if the restart was successful. 33 34 It does lead to another conundrum - what if due to some error, 4CAT never 35 restarts? Then this worker will not be run again to report its own failure. 36 There seem to be no clean ways around this, so anything watching the 37 outcome of the worker probably needs to implement some timeout after which 38 it is assumed that the restart/upgrade process failed catastrophically. 39 """ 40 type = "restart-4cat" 41 max_workers = 1 42 43 def work(self): 44 """ 45 Restart 4CAT and optionally upgrade it to the latest release 46 """ 47 # figure out if we're starting the restart or checking the result 48 # after 4cat has been restarted 49 is_resuming = self.job.data["attempts"] > 0 50 51 # prevent multiple restarts running at the same time which could blow 52 # up really fast 53 lock_file = Path(self.config.get("PATH_ROOT")).joinpath("config/restart.lock") 54 55 # this file has the log of the restart worker itself and is checked by 56 # the frontend to see how far we are 57 log_file_restart = Path(self.config.get("PATH_ROOT")).joinpath(self.config.get("PATH_LOGS")).joinpath("restart.log") 58 log_stream_restart = log_file_restart.open("a") 59 60 if not is_resuming: 61 log_stream_restart.write("Initiating 4CAT restart worker\n") 62 self.log.info("New restart initiated.") 63 64 # this lock file will ensure that people don't start two 65 # simultaneous upgrades or something 66 with lock_file.open("w") as outfile: 67 hasher = hashlib.blake2b() 68 hasher.update(str(uuid.uuid4()).encode("utf-8")) 69 outfile.write(hasher.hexdigest()) 70 71 # trigger a restart and/or upgrade 72 # returns a JSON with a 'status' key and a message, the message 73 # being the process output 74 75 if self.job.data["remote_id"].startswith("upgrade"): 76 command = sys.executable + " helper-scripts/migrate.py --repository %s --yes --restart" % \ 77 (oslex.quote(self.config.get("4cat.github_url"))) 78 if self.job.details and self.job.details.get("branch"): 79 # migrate to code in specific branch 80 command += f" --branch {oslex.quote(self.job.details['branch'])}" 81 else: 82 # migrate to latest release 83 command += " --release" 84 85 else: 86 command = sys.executable + " 4cat-daemon.py --no-version-check force-restart" 87 88 try: 89 # flush any writes before the other process starts writing to 90 # the stream 91 self.log.info(f"Running command {command}") 92 log_stream_restart.flush() 93 94 # the tricky part is that this command will interrupt the 95 # daemon, i.e. this worker! 96 # so we'll never get to actually send a response, if all goes 97 # well. but the file descriptor that stdout is piped to remains 98 # open, somehow, so we can use that to keep track of the output 99 # stdin needs to be /dev/null here because else when 4CAT 100 # restarts and we re-attempt to make a daemon, it will fail 101 # when trying to close the stdin file descriptor of the 102 # subprocess (man, that was a fun bug to hunt down) 103 process = subprocess.Popen(oslex.split(command), cwd=str(self.config.get("PATH_ROOT")), 104 stdout=log_stream_restart, stderr=log_stream_restart, 105 stdin=subprocess.DEVNULL) 106 107 while not self.interrupted: 108 # basically wait for either the process to quit or 4CAT to 109 # be restarted (hopefully the latter) 110 try: 111 # now see if the process is finished - if not a 112 # TimeoutExpired will be raised 113 process.wait(1) 114 break 115 except subprocess.TimeoutExpired: 116 pass 117 118 if process.returncode is not None: 119 # if we reach this, 4CAT was never restarted, and so the job failed 120 log_stream_restart.write( 121 f"\nUnexpected outcome of restart call ({process.returncode})\n") 122 123 raise RuntimeError() 124 else: 125 # interrupted before the process could finish (as it should) 126 self.log.info("Restart triggered. Restarting 4CAT.\n") 127 raise WorkerInterruptedException() 128 129 except (RuntimeError, subprocess.CalledProcessError) as e: 130 log_stream_restart.write(str(e)) 131 log_stream_restart.write( 132 "[Worker] Error while restarting 4CAT. The script returned a non-standard error code " 133 "(see above). You may need to restart 4CAT manually.\n") 134 self.log.error(f"Error restarting 4CAT. See {log_stream_restart.name} for details.") 135 lock_file.unlink() 136 self.job.finish() 137 138 finally: 139 log_stream_restart.close() 140 141 else: 142 # 4CAT back-end was restarted - now check the results and make the 143 # front-end restart or upgrade too 144 self.log.info("Restart worker resumed after restarting 4CAT, restart successful.") 145 log_stream_restart.write("4CAT restarted.\n") 146 with Path(self.config.get("PATH_ROOT")).joinpath("config/.current-version").open() as infile: 147 log_stream_restart.write(f"4CAT is now running version {infile.readline().strip()}.\n") 148 149 # we're gonna use some specific Flask routes to trigger this, i.e. 150 # we're interacting with the front-end through HTTP 151 api_host = "https://" if self.config.get("flask.https") else "http://" 152 if self.config.get("USING_DOCKER"): 153 import os 154 docker_exposed_port = os.environ['PUBLIC_PORT'] 155 api_host += f"host.docker.internal{':' + docker_exposed_port if docker_exposed_port != '80' else ''}" 156 else: 157 api_host += self.config.get("flask.server_name") 158 159 if self.job.data["remote_id"].startswith("upgrade") and self.config.get("USING_DOCKER"): 160 # when using Docker, the front-end needs to update separately 161 log_stream_restart.write("Telling front-end Docker container to upgrade...\n") 162 log_stream_restart.close() # close, because front-end will be writing to it 163 upgrade_ok = False 164 upgrade_timeout = False 165 try: 166 upgrade_url = api_host + "/admin/trigger-frontend-upgrade/" 167 with lock_file.open() as infile: 168 frontend_upgrade = requests.post(upgrade_url, data={"token": infile.read()}, timeout=(10 * 60)) 169 upgrade_ok = frontend_upgrade.json()["status"] == "OK" 170 except requests.RequestException: 171 pass 172 except TimeoutError: 173 upgrade_timeout = True 174 175 log_stream_restart = log_file_restart.open("a") 176 if not upgrade_ok: 177 if upgrade_timeout: 178 log_stream_restart.write("Upgrade timed out.") 179 log_stream_restart.write("Error upgrading front-end container. You may need to upgrade and restart" 180 "containers manually.\n") 181 lock_file.unlink() 182 return self.job.finish() 183 184 # restart front-end 185 log_stream_restart.write("Asking front-end to restart itself...\n") 186 log_stream_restart.flush() 187 try: 188 restart_url = api_host + "/admin/trigger-frontend-restart/" 189 with lock_file.open() as infile: 190 response = requests.post(restart_url, data={"token": infile.read()}, timeout=5).json() 191 192 if response.get("message"): 193 log_stream_restart.write(response.get("message") + "\n") 194 except (json.JSONDecodeError, requests.RequestException): 195 # this may happen because the server restarts and interrupts 196 # the request 197 pass 198 199 # wait for front-end to come online after a restart 200 time.sleep(3) # give some time for the restart to trigger 201 start_time = time.time() 202 frontend_ok = False 203 while time.time() < start_time + 60: 204 try: 205 frontend = requests.get(api_host + "/", timeout=5) 206 if frontend.status_code > 401: 207 time.sleep(2) 208 continue 209 frontend_ok = True 210 break 211 except requests.RequestException: 212 time.sleep(1) 213 continue 214 215 # too bad 216 if not frontend_ok: 217 log_stream_restart.write("Timed out waiting for front-end to restart. You may need to restart it " 218 "manually.\n") 219 self.log.error("Front-end did not come back online after restart") 220 else: 221 log_stream_restart.write("Front-end is available. Restart complete.") 222 self.log.info("Front-end is available. Restart complete.") 223 224 log_stream_restart.close() 225 lock_file.unlink() 226 227 self.job.finish()
Restart 4CAT and optionally upgrade it to the latest release
Why implement this as a worker? Trying to have 4CAT restart itself leads to an interesting conundrum: it will not be able to report the outcome of the restart, because whatever bit of code is keeping track of that will be interrupted by restarting 4CAT.
Using a worker has the benefit of it restarting after 4CAT restarts, so it can then figure out that 4CAT was just restarted and report the outcome. It then uses a log file to keep track of the results. The log file can then be used by other parts of 4CAT to see if the restart was successful.
It does lead to another conundrum - what if due to some error, 4CAT never restarts? Then this worker will not be run again to report its own failure. There seem to be no clean ways around this, so anything watching the outcome of the worker probably needs to implement some timeout after which it is assumed that the restart/upgrade process failed catastrophically.
43 def work(self): 44 """ 45 Restart 4CAT and optionally upgrade it to the latest release 46 """ 47 # figure out if we're starting the restart or checking the result 48 # after 4cat has been restarted 49 is_resuming = self.job.data["attempts"] > 0 50 51 # prevent multiple restarts running at the same time which could blow 52 # up really fast 53 lock_file = Path(self.config.get("PATH_ROOT")).joinpath("config/restart.lock") 54 55 # this file has the log of the restart worker itself and is checked by 56 # the frontend to see how far we are 57 log_file_restart = Path(self.config.get("PATH_ROOT")).joinpath(self.config.get("PATH_LOGS")).joinpath("restart.log") 58 log_stream_restart = log_file_restart.open("a") 59 60 if not is_resuming: 61 log_stream_restart.write("Initiating 4CAT restart worker\n") 62 self.log.info("New restart initiated.") 63 64 # this lock file will ensure that people don't start two 65 # simultaneous upgrades or something 66 with lock_file.open("w") as outfile: 67 hasher = hashlib.blake2b() 68 hasher.update(str(uuid.uuid4()).encode("utf-8")) 69 outfile.write(hasher.hexdigest()) 70 71 # trigger a restart and/or upgrade 72 # returns a JSON with a 'status' key and a message, the message 73 # being the process output 74 75 if self.job.data["remote_id"].startswith("upgrade"): 76 command = sys.executable + " helper-scripts/migrate.py --repository %s --yes --restart" % \ 77 (oslex.quote(self.config.get("4cat.github_url"))) 78 if self.job.details and self.job.details.get("branch"): 79 # migrate to code in specific branch 80 command += f" --branch {oslex.quote(self.job.details['branch'])}" 81 else: 82 # migrate to latest release 83 command += " --release" 84 85 else: 86 command = sys.executable + " 4cat-daemon.py --no-version-check force-restart" 87 88 try: 89 # flush any writes before the other process starts writing to 90 # the stream 91 self.log.info(f"Running command {command}") 92 log_stream_restart.flush() 93 94 # the tricky part is that this command will interrupt the 95 # daemon, i.e. this worker! 96 # so we'll never get to actually send a response, if all goes 97 # well. but the file descriptor that stdout is piped to remains 98 # open, somehow, so we can use that to keep track of the output 99 # stdin needs to be /dev/null here because else when 4CAT 100 # restarts and we re-attempt to make a daemon, it will fail 101 # when trying to close the stdin file descriptor of the 102 # subprocess (man, that was a fun bug to hunt down) 103 process = subprocess.Popen(oslex.split(command), cwd=str(self.config.get("PATH_ROOT")), 104 stdout=log_stream_restart, stderr=log_stream_restart, 105 stdin=subprocess.DEVNULL) 106 107 while not self.interrupted: 108 # basically wait for either the process to quit or 4CAT to 109 # be restarted (hopefully the latter) 110 try: 111 # now see if the process is finished - if not a 112 # TimeoutExpired will be raised 113 process.wait(1) 114 break 115 except subprocess.TimeoutExpired: 116 pass 117 118 if process.returncode is not None: 119 # if we reach this, 4CAT was never restarted, and so the job failed 120 log_stream_restart.write( 121 f"\nUnexpected outcome of restart call ({process.returncode})\n") 122 123 raise RuntimeError() 124 else: 125 # interrupted before the process could finish (as it should) 126 self.log.info("Restart triggered. Restarting 4CAT.\n") 127 raise WorkerInterruptedException() 128 129 except (RuntimeError, subprocess.CalledProcessError) as e: 130 log_stream_restart.write(str(e)) 131 log_stream_restart.write( 132 "[Worker] Error while restarting 4CAT. The script returned a non-standard error code " 133 "(see above). You may need to restart 4CAT manually.\n") 134 self.log.error(f"Error restarting 4CAT. See {log_stream_restart.name} for details.") 135 lock_file.unlink() 136 self.job.finish() 137 138 finally: 139 log_stream_restart.close() 140 141 else: 142 # 4CAT back-end was restarted - now check the results and make the 143 # front-end restart or upgrade too 144 self.log.info("Restart worker resumed after restarting 4CAT, restart successful.") 145 log_stream_restart.write("4CAT restarted.\n") 146 with Path(self.config.get("PATH_ROOT")).joinpath("config/.current-version").open() as infile: 147 log_stream_restart.write(f"4CAT is now running version {infile.readline().strip()}.\n") 148 149 # we're gonna use some specific Flask routes to trigger this, i.e. 150 # we're interacting with the front-end through HTTP 151 api_host = "https://" if self.config.get("flask.https") else "http://" 152 if self.config.get("USING_DOCKER"): 153 import os 154 docker_exposed_port = os.environ['PUBLIC_PORT'] 155 api_host += f"host.docker.internal{':' + docker_exposed_port if docker_exposed_port != '80' else ''}" 156 else: 157 api_host += self.config.get("flask.server_name") 158 159 if self.job.data["remote_id"].startswith("upgrade") and self.config.get("USING_DOCKER"): 160 # when using Docker, the front-end needs to update separately 161 log_stream_restart.write("Telling front-end Docker container to upgrade...\n") 162 log_stream_restart.close() # close, because front-end will be writing to it 163 upgrade_ok = False 164 upgrade_timeout = False 165 try: 166 upgrade_url = api_host + "/admin/trigger-frontend-upgrade/" 167 with lock_file.open() as infile: 168 frontend_upgrade = requests.post(upgrade_url, data={"token": infile.read()}, timeout=(10 * 60)) 169 upgrade_ok = frontend_upgrade.json()["status"] == "OK" 170 except requests.RequestException: 171 pass 172 except TimeoutError: 173 upgrade_timeout = True 174 175 log_stream_restart = log_file_restart.open("a") 176 if not upgrade_ok: 177 if upgrade_timeout: 178 log_stream_restart.write("Upgrade timed out.") 179 log_stream_restart.write("Error upgrading front-end container. You may need to upgrade and restart" 180 "containers manually.\n") 181 lock_file.unlink() 182 return self.job.finish() 183 184 # restart front-end 185 log_stream_restart.write("Asking front-end to restart itself...\n") 186 log_stream_restart.flush() 187 try: 188 restart_url = api_host + "/admin/trigger-frontend-restart/" 189 with lock_file.open() as infile: 190 response = requests.post(restart_url, data={"token": infile.read()}, timeout=5).json() 191 192 if response.get("message"): 193 log_stream_restart.write(response.get("message") + "\n") 194 except (json.JSONDecodeError, requests.RequestException): 195 # this may happen because the server restarts and interrupts 196 # the request 197 pass 198 199 # wait for front-end to come online after a restart 200 time.sleep(3) # give some time for the restart to trigger 201 start_time = time.time() 202 frontend_ok = False 203 while time.time() < start_time + 60: 204 try: 205 frontend = requests.get(api_host + "/", timeout=5) 206 if frontend.status_code > 401: 207 time.sleep(2) 208 continue 209 frontend_ok = True 210 break 211 except requests.RequestException: 212 time.sleep(1) 213 continue 214 215 # too bad 216 if not frontend_ok: 217 log_stream_restart.write("Timed out waiting for front-end to restart. You may need to restart it " 218 "manually.\n") 219 self.log.error("Front-end did not come back online after restart") 220 else: 221 log_stream_restart.write("Front-end is available. Restart complete.") 222 self.log.info("Front-end is available. Restart complete.") 223 224 log_stream_restart.close() 225 lock_file.unlink() 226 227 self.job.finish()
Restart 4CAT and optionally upgrade it to the latest release