1*b2d29daeSVladimir Kotal#!/usr/bin/env python3 2*b2d29daeSVladimir Kotal 3*b2d29daeSVladimir Kotal# CDDL HEADER START 4*b2d29daeSVladimir Kotal# 5*b2d29daeSVladimir Kotal# The contents of this file are subject to the terms of the 6*b2d29daeSVladimir Kotal# Common Development and Distribution License (the "License"). 7*b2d29daeSVladimir Kotal# You may not use this file except in compliance with the License. 8*b2d29daeSVladimir Kotal# 9*b2d29daeSVladimir Kotal# See LICENSE.txt included in this distribution for the specific 10*b2d29daeSVladimir Kotal# language governing permissions and limitations under the License. 11*b2d29daeSVladimir Kotal# 12*b2d29daeSVladimir Kotal# When distributing Covered Code, include this CDDL HEADER in each 13*b2d29daeSVladimir Kotal# file and include the License file at LICENSE.txt. 14*b2d29daeSVladimir Kotal# If applicable, add the following below this CDDL HEADER, with the 15*b2d29daeSVladimir Kotal# fields enclosed by brackets "[]" replaced with your own identifying 16*b2d29daeSVladimir Kotal# information: Portions Copyright [yyyy] [name of copyright owner] 17*b2d29daeSVladimir Kotal# 18*b2d29daeSVladimir Kotal# CDDL HEADER END 19*b2d29daeSVladimir Kotal 20*b2d29daeSVladimir Kotal# 21*b2d29daeSVladimir Kotal# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. 22*b2d29daeSVladimir Kotal# 23*b2d29daeSVladimir Kotal 24*b2d29daeSVladimir Kotalimport os 25*b2d29daeSVladimir Kotalfrom requests import get, ConnectionError 26*b2d29daeSVladimir Kotalimport logging 27*b2d29daeSVladimir Kotalimport multiprocessing 28*b2d29daeSVladimir Kotalfrom pathlib import Path 29*b2d29daeSVladimir Kotalimport shutil 30*b2d29daeSVladimir Kotalimport subprocess 31*b2d29daeSVladimir Kotalimport threading 32*b2d29daeSVladimir Kotalimport time 33*b2d29daeSVladimir Kotal 34*b2d29daeSVladimir Kotalfrom opengrok_tools.utils.log import get_console_logger, \ 35*b2d29daeSVladimir Kotal get_log_level, get_class_basename 36*b2d29daeSVladimir Kotalfrom opengrok_tools.deploy import deploy_war 37*b2d29daeSVladimir Kotalfrom opengrok_tools.utils.indexer import Indexer 38*b2d29daeSVladimir Kotalfrom opengrok_tools.sync import do_sync 39*b2d29daeSVladimir Kotalfrom opengrok_tools.utils.opengrok import list_projects, \ 40*b2d29daeSVladimir Kotal add_project, delete_project, get_configuration 41*b2d29daeSVladimir Kotalfrom opengrok_tools.utils.readconfig import read_config 42*b2d29daeSVladimir Kotalfrom opengrok_tools.utils.exitvals import SUCCESS_EXITVAL 43*b2d29daeSVladimir Kotal 44*b2d29daeSVladimir Kotal 45*b2d29daeSVladimir Kotalfs_root = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep 46*b2d29daeSVladimir Kotalif os.environ.get('OPENGROK_TOMCAT_ROOT'): # debug only 47*b2d29daeSVladimir Kotal tomcat_root = os.environ.get('OPENGROK_TOMCAT_ROOT') 48*b2d29daeSVladimir Kotalelse: 49*b2d29daeSVladimir Kotal tomcat_root = os.path.join(fs_root, "usr", "local", "tomcat") 50*b2d29daeSVladimir Kotal 51*b2d29daeSVladimir Kotalif os.environ.get('OPENGROK_ROOT'): # debug only 52*b2d29daeSVladimir Kotal OPENGROK_BASE_DIR = os.environ.get('OPENGROK_ROOT') 53*b2d29daeSVladimir Kotalelse: 54*b2d29daeSVladimir Kotal OPENGROK_BASE_DIR = os.path.join(fs_root, "opengrok") 55*b2d29daeSVladimir Kotal 56*b2d29daeSVladimir KotalOPENGROK_LIB_DIR = os.path.join(OPENGROK_BASE_DIR, "lib") 57*b2d29daeSVladimir KotalOPENGROK_DATA_ROOT = os.path.join(OPENGROK_BASE_DIR, "data") 58*b2d29daeSVladimir KotalOPENGROK_SRC_ROOT = os.path.join(OPENGROK_BASE_DIR, "src") 59*b2d29daeSVladimir KotalBODY_INCLUDE_FILE = os.path.join(OPENGROK_DATA_ROOT, "body_include") 60*b2d29daeSVladimir KotalOPENGROK_CONFIG_FILE = os.path.join(OPENGROK_BASE_DIR, "etc", 61*b2d29daeSVladimir Kotal "configuration.xml") 62*b2d29daeSVladimir KotalOPENGROK_WEBAPPS_DIR = os.path.join(tomcat_root, "webapps") 63*b2d29daeSVladimir Kotal 64*b2d29daeSVladimir Kotal 65*b2d29daeSVladimir Kotaldef set_url_root(url_root): 66*b2d29daeSVladimir Kotal """ 67*b2d29daeSVladimir Kotal Set URL root and URI based on input 68*b2d29daeSVladimir Kotal :param url_root: input 69*b2d29daeSVladimir Kotal :return: URI and URL root 70*b2d29daeSVladimir Kotal """ 71*b2d29daeSVladimir Kotal if not url_root: 72*b2d29daeSVladimir Kotal url_root = '/' 73*b2d29daeSVladimir Kotal 74*b2d29daeSVladimir Kotal if ' ' in url_root: 75*b2d29daeSVladimir Kotal logger.warn('Deployment path contains spaces. Deploying to root') 76*b2d29daeSVladimir Kotal url_root = '/' 77*b2d29daeSVladimir Kotal 78*b2d29daeSVladimir Kotal # Remove leading and trailing slashes 79*b2d29daeSVladimir Kotal if url_root.startswith('/'): 80*b2d29daeSVladimir Kotal url_root = url_root[1:] 81*b2d29daeSVladimir Kotal if url_root.endswith('/'): 82*b2d29daeSVladimir Kotal url_root = url_root[:-1] 83*b2d29daeSVladimir Kotal 84*b2d29daeSVladimir Kotal uri = "http://localhost:8080/" + url_root 85*b2d29daeSVladimir Kotal # 86*b2d29daeSVladimir Kotal # Make sure URI ends with slash. This is important for the various API 87*b2d29daeSVladimir Kotal # calls, notably for those that check the HTTP error code. 88*b2d29daeSVladimir Kotal # Normally accessing the URI without the terminating slash results in 89*b2d29daeSVladimir Kotal # HTTP redirect (code 302) instead of success (200). 90*b2d29daeSVladimir Kotal # 91*b2d29daeSVladimir Kotal if not uri.endswith('/'): 92*b2d29daeSVladimir Kotal uri = uri + '/' 93*b2d29daeSVladimir Kotal 94*b2d29daeSVladimir Kotal return uri, url_root 95*b2d29daeSVladimir Kotal 96*b2d29daeSVladimir Kotal 97*b2d29daeSVladimir Kotaldef get_war_name(url_root): 98*b2d29daeSVladimir Kotal if len(url_root) == 0: 99*b2d29daeSVladimir Kotal return "ROOT.war" 100*b2d29daeSVladimir Kotal else: 101*b2d29daeSVladimir Kotal return url_root + ".war" 102*b2d29daeSVladimir Kotal 103*b2d29daeSVladimir Kotal 104*b2d29daeSVladimir Kotaldef deploy(url_root): 105*b2d29daeSVladimir Kotal """ 106*b2d29daeSVladimir Kotal Deploy the web application 107*b2d29daeSVladimir Kotal :param url_root: web app URL root 108*b2d29daeSVladimir Kotal """ 109*b2d29daeSVladimir Kotal 110*b2d29daeSVladimir Kotal logger.info('Deploying web application') 111*b2d29daeSVladimir Kotal webapps_dir = os.path.join(tomcat_root, 'webapps') 112*b2d29daeSVladimir Kotal if not os.path.isdir(webapps_dir): 113*b2d29daeSVladimir Kotal raise Exception("{} is not a directory".format(webapps_dir)) 114*b2d29daeSVladimir Kotal 115*b2d29daeSVladimir Kotal for item in os.listdir(webapps_dir): 116*b2d29daeSVladimir Kotal subdir = os.path.join(webapps_dir, item) 117*b2d29daeSVladimir Kotal if os.path.isdir(subdir): 118*b2d29daeSVladimir Kotal logger.debug("Removing '{}' directory recursively".format(subdir)) 119*b2d29daeSVladimir Kotal shutil.rmtree(subdir) 120*b2d29daeSVladimir Kotal 121*b2d29daeSVladimir Kotal deploy_war(logger, os.path.join(OPENGROK_LIB_DIR, "source.war"), 122*b2d29daeSVladimir Kotal os.path.join(OPENGROK_WEBAPPS_DIR, get_war_name(url_root)), 123*b2d29daeSVladimir Kotal OPENGROK_CONFIG_FILE, None) 124*b2d29daeSVladimir Kotal 125*b2d29daeSVladimir Kotal 126*b2d29daeSVladimir Kotaldef setup_redirect_source(url_root): 127*b2d29daeSVladimir Kotal """ 128*b2d29daeSVladimir Kotal Set up redirect from /source 129*b2d29daeSVladimir Kotal """ 130*b2d29daeSVladimir Kotal logger.debug("Setting up redirect from /source to '{}'".format(url_root)) 131*b2d29daeSVladimir Kotal source_dir = os.path.join(OPENGROK_WEBAPPS_DIR, "source") 132*b2d29daeSVladimir Kotal if not os.path.isdir(source_dir): 133*b2d29daeSVladimir Kotal os.makedirs(source_dir) 134*b2d29daeSVladimir Kotal 135*b2d29daeSVladimir Kotal with open(os.path.join(source_dir, "index.jsp"), "w+") as index: 136*b2d29daeSVladimir Kotal index.write("<% response.sendRedirect(\"/{}\"); %>".format(url_root)) 137*b2d29daeSVladimir Kotal 138*b2d29daeSVladimir Kotal 139*b2d29daeSVladimir Kotaldef wait_for_tomcat(uri): 140*b2d29daeSVladimir Kotal """ 141*b2d29daeSVladimir Kotal Active/busy waiting for Tomcat to come up. 142*b2d29daeSVladimir Kotal Currently there is no upper time bound. 143*b2d29daeSVladimir Kotal """ 144*b2d29daeSVladimir Kotal logger.info("Waiting for Tomcat to start") 145*b2d29daeSVladimir Kotal 146*b2d29daeSVladimir Kotal while True: 147*b2d29daeSVladimir Kotal try: 148*b2d29daeSVladimir Kotal r = get(uri) 149*b2d29daeSVladimir Kotal status = r.status_code 150*b2d29daeSVladimir Kotal except ConnectionError: 151*b2d29daeSVladimir Kotal status = 0 152*b2d29daeSVladimir Kotal 153*b2d29daeSVladimir Kotal if status != 200: 154*b2d29daeSVladimir Kotal logger.debug("Got status {} for {}, sleeping for 1 second". 155*b2d29daeSVladimir Kotal format(status, uri)) 156*b2d29daeSVladimir Kotal time.sleep(1) 157*b2d29daeSVladimir Kotal else: 158*b2d29daeSVladimir Kotal break 159*b2d29daeSVladimir Kotal 160*b2d29daeSVladimir Kotal logger.info("Tomcat is ready") 161*b2d29daeSVladimir Kotal 162*b2d29daeSVladimir Kotal 163*b2d29daeSVladimir Kotaldef refresh_projects(uri): 164*b2d29daeSVladimir Kotal """ 165*b2d29daeSVladimir Kotal Ensure each immediate source root subdirectory is a project. 166*b2d29daeSVladimir Kotal """ 167*b2d29daeSVladimir Kotal webapp_projects = list_projects(logger, uri) 168*b2d29daeSVladimir Kotal logger.debug('Projects from the web app: {}'.format(webapp_projects)) 169*b2d29daeSVladimir Kotal src_root = OPENGROK_SRC_ROOT 170*b2d29daeSVladimir Kotal 171*b2d29daeSVladimir Kotal # Add projects. 172*b2d29daeSVladimir Kotal for item in os.listdir(src_root): 173*b2d29daeSVladimir Kotal logger.debug('Got item {}'.format(item)) 174*b2d29daeSVladimir Kotal if os.path.isdir(os.path.join(src_root, item)): 175*b2d29daeSVladimir Kotal if item not in webapp_projects: 176*b2d29daeSVladimir Kotal logger.info("Adding project {}".format(item)) 177*b2d29daeSVladimir Kotal add_project(logger, item, uri) 178*b2d29daeSVladimir Kotal 179*b2d29daeSVladimir Kotal # Remove projects 180*b2d29daeSVladimir Kotal for item in webapp_projects: 181*b2d29daeSVladimir Kotal if not os.path.isdir(os.path.join(src_root, item)): 182*b2d29daeSVladimir Kotal logger.info("Deleting project {}".format(item)) 183*b2d29daeSVladimir Kotal delete_project(logger, item, uri) 184*b2d29daeSVladimir Kotal 185*b2d29daeSVladimir Kotal 186*b2d29daeSVladimir Kotaldef save_config(uri, config_path): 187*b2d29daeSVladimir Kotal """ 188*b2d29daeSVladimir Kotal Retrieve configuration from the web app and write it to file. 189*b2d29daeSVladimir Kotal :param uri: web app URI 190*b2d29daeSVladimir Kotal :param config_path: file path 191*b2d29daeSVladimir Kotal """ 192*b2d29daeSVladimir Kotal 193*b2d29daeSVladimir Kotal logger.info('Saving configuration to {}'.format(config_path)) 194*b2d29daeSVladimir Kotal config = get_configuration(logger, uri) 195*b2d29daeSVladimir Kotal with open(config_path, "w+") as config_file: 196*b2d29daeSVladimir Kotal config_file.write(config) 197*b2d29daeSVladimir Kotal 198*b2d29daeSVladimir Kotal 199*b2d29daeSVladimir Kotaldef merge_commands_env(commands, env): 200*b2d29daeSVladimir Kotal """ 201*b2d29daeSVladimir Kotal Merge environment into command structure. If any of the commands has 202*b2d29daeSVladimir Kotal an environment already set, the env is merged in. 203*b2d29daeSVladimir Kotal :param commands: commands structure 204*b2d29daeSVladimir Kotal :param env: environment dictionary 205*b2d29daeSVladimir Kotal :return: updated commands structure 206*b2d29daeSVladimir Kotal """ 207*b2d29daeSVladimir Kotal for cmd in commands: 208*b2d29daeSVladimir Kotal cmd_env = cmd.get('env') 209*b2d29daeSVladimir Kotal if cmd_env: 210*b2d29daeSVladimir Kotal cmd.env.update(env) 211*b2d29daeSVladimir Kotal else: 212*b2d29daeSVladimir Kotal cmd['env'] = env 213*b2d29daeSVladimir Kotal 214*b2d29daeSVladimir Kotal return commands 215*b2d29daeSVladimir Kotal 216*b2d29daeSVladimir Kotal 217*b2d29daeSVladimir Kotaldef syncer(loglevel, uri, config_path, reindex, numworkers, env): 218*b2d29daeSVladimir Kotal """ 219*b2d29daeSVladimir Kotal Wrapper for running opengrok-sync. 220*b2d29daeSVladimir Kotal To be run in a thread/process in the background. 221*b2d29daeSVladimir Kotal """ 222*b2d29daeSVladimir Kotal 223*b2d29daeSVladimir Kotal wait_for_tomcat(uri) 224*b2d29daeSVladimir Kotal 225*b2d29daeSVladimir Kotal while True: 226*b2d29daeSVladimir Kotal refresh_projects(uri) 227*b2d29daeSVladimir Kotal 228*b2d29daeSVladimir Kotal if os.environ.get('OPENGROK_SYNC_YML'): # debug only 229*b2d29daeSVladimir Kotal config_file = os.environ.get('OPENGROK_SYNC_YML') 230*b2d29daeSVladimir Kotal else: 231*b2d29daeSVladimir Kotal config_file = os.path.join(fs_root, 'scripts', 'sync.yml') 232*b2d29daeSVladimir Kotal config = read_config(logger, config_file) 233*b2d29daeSVladimir Kotal if config is None: 234*b2d29daeSVladimir Kotal logger.error("Cannot read config file from {}".format(config_file)) 235*b2d29daeSVladimir Kotal raise Exception("no sync config") 236*b2d29daeSVladimir Kotal 237*b2d29daeSVladimir Kotal projects = list_projects(logger, uri) 238*b2d29daeSVladimir Kotal # 239*b2d29daeSVladimir Kotal # The driveon=True is needed for the initial indexing of newly 240*b2d29daeSVladimir Kotal # added project, otherwise the incoming check in the opengrok-mirror 241*b2d29daeSVladimir Kotal # would short circuit it. 242*b2d29daeSVladimir Kotal # 243*b2d29daeSVladimir Kotal if env: 244*b2d29daeSVladimir Kotal logger.info('Merging commands with environment') 245*b2d29daeSVladimir Kotal commands = merge_commands_env(config["commands"], env) 246*b2d29daeSVladimir Kotal logger.debug(config['commands']) 247*b2d29daeSVladimir Kotal else: 248*b2d29daeSVladimir Kotal commands = config["commands"] 249*b2d29daeSVladimir Kotal 250*b2d29daeSVladimir Kotal logger.info("Sync starting") 251*b2d29daeSVladimir Kotal do_sync(loglevel, commands, config.get('cleanup'), 252*b2d29daeSVladimir Kotal projects, config.get("ignore_errors"), uri, 253*b2d29daeSVladimir Kotal numworkers, driveon=True, logger=logger, print_output=True) 254*b2d29daeSVladimir Kotal logger.info("Sync done") 255*b2d29daeSVladimir Kotal 256*b2d29daeSVladimir Kotal # Workaround for https://github.com/oracle/opengrok/issues/1670 257*b2d29daeSVladimir Kotal Path(os.path.join(OPENGROK_DATA_ROOT, 'timestamp')).touch() 258*b2d29daeSVladimir Kotal 259*b2d29daeSVladimir Kotal save_config(uri, config_path) 260*b2d29daeSVladimir Kotal 261*b2d29daeSVladimir Kotal sleep_seconds = int(reindex) * 60 262*b2d29daeSVladimir Kotal logger.info("Sleeping for {} seconds".format(sleep_seconds)) 263*b2d29daeSVladimir Kotal time.sleep(sleep_seconds) 264*b2d29daeSVladimir Kotal 265*b2d29daeSVladimir Kotal 266*b2d29daeSVladimir Kotaldef create_bare_config(): 267*b2d29daeSVladimir Kotal """ 268*b2d29daeSVladimir Kotal Create bare configuration file with a few basic settings. 269*b2d29daeSVladimir Kotal """ 270*b2d29daeSVladimir Kotal 271*b2d29daeSVladimir Kotal logger.info('Creating bare configuration in {}'. 272*b2d29daeSVladimir Kotal format(OPENGROK_CONFIG_FILE)) 273*b2d29daeSVladimir Kotal indexer = Indexer(['-s', OPENGROK_SRC_ROOT, 274*b2d29daeSVladimir Kotal '-d', OPENGROK_DATA_ROOT, 275*b2d29daeSVladimir Kotal '-c', '/usr/local/bin/ctags', 276*b2d29daeSVladimir Kotal '--remote', 'on', 277*b2d29daeSVladimir Kotal '-P', '-H', 278*b2d29daeSVladimir Kotal '-W', OPENGROK_CONFIG_FILE, 279*b2d29daeSVladimir Kotal '--noIndex'], 280*b2d29daeSVladimir Kotal jar=os.path.join(OPENGROK_LIB_DIR, 281*b2d29daeSVladimir Kotal 'opengrok.jar'), 282*b2d29daeSVladimir Kotal logger=logger, doprint=True) 283*b2d29daeSVladimir Kotal indexer.execute() 284*b2d29daeSVladimir Kotal ret = indexer.getretcode() 285*b2d29daeSVladimir Kotal if ret != SUCCESS_EXITVAL: 286*b2d29daeSVladimir Kotal logger.error('Command returned {}'.format(ret)) 287*b2d29daeSVladimir Kotal logger.error(indexer.geterroutput()) 288*b2d29daeSVladimir Kotal raise Exception("Failed to create bare configuration") 289*b2d29daeSVladimir Kotal 290*b2d29daeSVladimir Kotal 291*b2d29daeSVladimir Kotalif __name__ == "__main__": 292*b2d29daeSVladimir Kotal log_level = os.environ.get('OPENGROK_LOG_LEVEL') 293*b2d29daeSVladimir Kotal if log_level: 294*b2d29daeSVladimir Kotal log_level = get_log_level(log_level) 295*b2d29daeSVladimir Kotal else: 296*b2d29daeSVladimir Kotal log_level = logging.INFO 297*b2d29daeSVladimir Kotal 298*b2d29daeSVladimir Kotal logger = get_console_logger(get_class_basename(), log_level) 299*b2d29daeSVladimir Kotal 300*b2d29daeSVladimir Kotal URI, URL_ROOT = set_url_root(os.environ.get('URL_ROOT')) 301*b2d29daeSVladimir Kotal logger.debug("URL_ROOT = {}".format(URL_ROOT)) 302*b2d29daeSVladimir Kotal logger.debug("URI = {}".format(URI)) 303*b2d29daeSVladimir Kotal 304*b2d29daeSVladimir Kotal # default period for reindexing (in minutes) 305*b2d29daeSVladimir Kotal reindex_env = os.environ.get('REINDEX') 306*b2d29daeSVladimir Kotal if reindex_env: 307*b2d29daeSVladimir Kotal reindex_min = reindex_env 308*b2d29daeSVladimir Kotal else: 309*b2d29daeSVladimir Kotal reindex_min = 10 310*b2d29daeSVladimir Kotal logger.debug("reindex period = {} minutes".format(reindex_min)) 311*b2d29daeSVladimir Kotal 312*b2d29daeSVladimir Kotal # Note that deploy is done before Tomcat is started. 313*b2d29daeSVladimir Kotal deploy(URL_ROOT) 314*b2d29daeSVladimir Kotal 315*b2d29daeSVladimir Kotal if URL_ROOT != '/source': 316*b2d29daeSVladimir Kotal setup_redirect_source(URL_ROOT) 317*b2d29daeSVladimir Kotal 318*b2d29daeSVladimir Kotal # TODO verify these are passed on 319*b2d29daeSVladimir Kotal env = {} 320*b2d29daeSVladimir Kotal if os.environ.get('INDEXER_OPT'): 321*b2d29daeSVladimir Kotal env['OPENGROK_INDEXER_OPTIONAL_ARGS'] = \ 322*b2d29daeSVladimir Kotal os.environ.get('INDEXER_OPT') 323*b2d29daeSVladimir Kotal if os.environ.get('NOMIRROR'): 324*b2d29daeSVladimir Kotal env['OPENGROK_NO_MIRROR'] = os.environ.get('NOMIRROR') 325*b2d29daeSVladimir Kotal logger.debug('Extra environment: {}'.format(env)) 326*b2d29daeSVladimir Kotal 327*b2d29daeSVladimir Kotal # 328*b2d29daeSVladimir Kotal # Create empty configuration to avoid the non existent file exception 329*b2d29daeSVladimir Kotal # in the web app during the first web app startup. 330*b2d29daeSVladimir Kotal # 331*b2d29daeSVladimir Kotal if not os.path.exists(OPENGROK_CONFIG_FILE) or \ 332*b2d29daeSVladimir Kotal os.path.getsize(OPENGROK_CONFIG_FILE) == 0: 333*b2d29daeSVladimir Kotal create_bare_config() 334*b2d29daeSVladimir Kotal 335*b2d29daeSVladimir Kotal if os.environ.get('WORKERS'): 336*b2d29daeSVladimir Kotal num_workers = os.environ.get('WORKERS') 337*b2d29daeSVladimir Kotal else: 338*b2d29daeSVladimir Kotal num_workers = multiprocessing.cpu_count() 339*b2d29daeSVladimir Kotal logger.info('Number of sync workers: {}'.format(num_workers)) 340*b2d29daeSVladimir Kotal 341*b2d29daeSVladimir Kotal logger.debug("Starting sync thread") 342*b2d29daeSVladimir Kotal t = threading.Thread(target=syncer, name="Sync thread", 343*b2d29daeSVladimir Kotal args=(log_level, URI, OPENGROK_CONFIG_FILE, 344*b2d29daeSVladimir Kotal reindex_min, num_workers, env)) 345*b2d29daeSVladimir Kotal t.start() 346*b2d29daeSVladimir Kotal 347*b2d29daeSVladimir Kotal # Start Tomcat last. It will be the foreground process. 348*b2d29daeSVladimir Kotal logger.info("Starting Tomcat") 349*b2d29daeSVladimir Kotal subprocess.run([os.path.join(tomcat_root, 'bin', 'catalina.sh'), 'run']) 350