#!/usr/bin/env python3 # Copyright (C) 2020 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse import json import math import os import subprocess import sys import time import reporelaypy import webkitcorepy scripts = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if os.path.isdir(os.path.join(scripts, 'webkitpy')): sys.path.insert(0, scripts) import webkitpy from reporelaypy import Checkout, Database, HookProcessor, HookReceiver, Redirector from webkitcorepy import arguments, AutoInstall from whichcraft import which def main(args=None): parser = argparse.ArgumentParser(description='Run a git relay server for testing') group = parser.add_argument_group('Repository') group.add_argument( '--path', '-p', '-C', dest='path', default=os.getcwd(), action='store', help='Set the repository path to be used', ) group.add_argument( '--http-proxy', dest='http_proxy', default=None, action='store', help='Set an http proxy', ) group.add_argument( '--url', dest='url', default=None, action='store', help='Set the repository path to be used', ) parser.add_argument( '--sentinal', '--no-sentinal', action=arguments.NoAction, dest='sentinal', default=False, help='Lay down sentinal file on disk to determine checkout state.', ) group.add_argument( '--update', dest='update_interval', default=0, action='store', type=int, help='Set the number of seconds to wait between polling git checkout', ) group.add_argument( '--fallback', dest='fallback', default=None, action='store', type=str, help='Fallback repository URL', ) group.add_argument( '--remote', dest='remotes', action='append', help="Add an additional remote to forward changes to, formatted as 'name:url'", ) group = parser.add_argument_group('Database') group.add_argument( '--redis-host', dest='redis_host', default=None, action='store', help="Set the hostname of the Redis database to be used. (Environment variable '{}' used by default)".format(Database.HOST_ENV), ) group.add_argument( '--redis-password', dest='redis_password', default=None, action='store', help="Set the password of the Redis database to be used. (Environment variable '{}' used by default)".format(Database.PASSWORD_ENV), ) group.add_argument( '--redis-expiration', dest='redis_expiration', default=Database.DEFAULT_EXPIRATION, action='store', type=int, help="Set the default Redis expiration time in seconds", ) group = parser.add_argument_group('Webserver') group.add_argument( '--port', dest='port', default=5000, action='store', help='Port to expose webserver on.', ) group.add_argument( '--poll', dest='poll', default=10, action='store', help='Number of seconds between polls of the webserver process.', ) group.add_argument( '--redirector', '-r', dest='redirector', action='append', help='Base URL to redirect user to for commit information.', ) group = parser.add_argument_group('Hooks') group.add_argument( '--hooks', '--no-hooks', action=arguments.NoAction, dest='hooks', default=False, help='Enable or disable hook end-points (disabled by default)', ) group.add_argument( '--debug', '--no-debug', action=arguments.NoAction, dest='hooks_debug', default=True, help='Enable endpoint to report hooks being processed (enabled by default)', ) args = parser.parse_args(args=args) database = Database(host=args.redis_host, password=args.redis_password) print('Constructed database with parameters:') print(' Host: {}'.format(database.host or 'None (in-memory fake database)')) if database.password: print(' Password: {}'.format(len(database.password) * '*')) if database.default_expiration: print(' Expiration: {} hours'.format(database.default_expiration / (60.0 * 60.0))) print('Connecting to database...') database.ping() print('Connected to database!') remotes = {} for pair in args.remotes: name, remote = pair.split(':', 1) if not remote: sys.stderr("Invalid forwarding remote '{}'".format(pair)) return 1 remotes[name] = remote print('Finding checkout...') checkout = Checkout( path=args.path, url=args.url, http_proxy=args.http_proxy, sentinal=args.sentinal, fallback_url=args.fallback, remotes=remotes, ) print('Git checkout:') print(' Path: {}'.format(checkout.path)) print(' URL: {}'.format(checkout.url)) if checkout.fallback_repository: print(' Fallback: {}'.format(checkout.fallback_repository.url)) if not checkout.repository: print(' Cloning repository...') if args.update_interval: print(' Polling checkout with interval of {} seconds, doing first poll now...'.format(args.update_interval)) checkout.update_all() if remotes: print('Forwarding updates to:') for name, url in remotes.items(): print(' {}: {}'.format(name, url)) if args.redirector: print('User specified the following redirect urls:') for url in args.redirector: redirector = Redirector(url) print(' {}: {}'.format(redirector.name, redirector.url)) passenv = dict( PYTHONPATH=':'.join(sys.path), CHECKOUT=json.dumps(checkout, cls=Checkout.Encoder), REDIRECTORS=json.dumps([Redirector(url) for url in args.redirector or []], cls=Redirector.Encoder), HOOKS=json.dumps({'enabled': args.hooks, 'debug': args.hooks_debug}), ) if AutoInstall.directory: passenv['AUTOINSTALL_PATH'] = AutoInstall.directory if database.host: passenv[database.HOST_ENV] = database.host if database.password: passenv[database.PASSWORD_ENV] = database.password if database.default_expiration: passenv[database.EXPIRATION_ENV] = str(database.default_expiration) if os.environ.get(HookReceiver.SECRET_ENV): passenv[HookReceiver.SECRET_ENV] = os.environ.get(HookReceiver.SECRET_ENV) processor = HookProcessor(checkout=checkout, database=database) if args.hooks else None with subprocess.Popen( [which('gunicorn'), 'reporelaypy.webserver:app'], cwd=os.path.dirname(os.path.dirname(reporelaypy.__file__)), env=passenv, ) as webserver: last_poll = time.time() last_pull = time.time() while True: if last_poll + args.poll < time.time(): if webserver.poll() is None: break last_poll = time.time() if last_pull + args.update_interval < time.time(): processor.process_hooks() if processor else checkout.update_all() last_pull = time.time() time.sleep(math.gcd(args.poll, args.update_interval)) return 0 if '__main__' == __name__: sys.exit(main(args=sys.argv[1:]))