예제 #1
0
def main(personal_access_token):
    """Updates the files corresponding to LOCAL_FECONF_PATH and
    LOCAL_CONSTANTS_PATH after doing the prerequisite checks.

    Args:
        personal_access_token: str. The personal access token for the
            GitHub id of user.
    """
    # Do prerequisite checks.
    common.require_cwd_to_be_oppia()
    assert common.is_current_branch_a_release_branch()
    common.ensure_release_scripts_folder_exists_and_is_up_to_date()
    try:
        python_utils.url_open(TERMS_PAGE_URL)
    except Exception:
        raise Exception('Terms mainpage does not exist on Github.')

    try:
        check_updates_to_terms_of_service(personal_access_token)
        add_mailgun_api_key()

        apply_changes_based_on_config(LOCAL_FECONF_PATH, FECONF_CONFIG_PATH,
                                      FECONF_REGEX)
        apply_changes_based_on_config(LOCAL_CONSTANTS_PATH,
                                      CONSTANTS_CONFIG_PATH, CONSTANTS_REGEX)
    except Exception as e:
        common.run_cmd(
            ['git', 'checkout', '--', LOCAL_FECONF_PATH, LOCAL_CONSTANTS_PATH])
        raise Exception(e)

    common.ask_user_to_confirm(
        'Done! Please check manually to ensure all the changes are correct.')
예제 #2
0
def main(args=None):
    """Updates the files corresponding to LOCAL_FECONF_PATH and
    LOCAL_CONSTANTS_PATH after doing the prerequisite checks.
    """
    options = _PARSER.parse_args(args=args)

    # Do prerequisite checks.
    feconf_config_path = os.path.join(
        options.deploy_data_path, 'feconf_updates.config')
    constants_config_path = os.path.join(
        options.deploy_data_path, 'constants_updates.config')

    release_feconf_path = os.path.join(
        options.release_dir_path, common.FECONF_PATH)
    release_constants_path = os.path.join(
        options.release_dir_path, common.CONSTANTS_FILE_PATH)

    if options.prompt_for_mailgun_and_terms_update:
        try:
            python_utils.url_open(TERMS_PAGE_FOLDER_URL)
        except Exception:
            raise Exception('Terms mainpage does not exist on Github.')
        add_mailgun_api_key(release_feconf_path)
        add_mailchimp_api_key(release_feconf_path)
        check_updates_to_terms_of_service(
            release_feconf_path, options.personal_access_token)

    apply_changes_based_on_config(
        release_feconf_path, feconf_config_path, FECONF_REGEX)
    apply_changes_based_on_config(
        release_constants_path, constants_config_path, CONSTANTS_REGEX)
    verify_feconf(
        release_feconf_path, options.prompt_for_mailgun_and_terms_update)
예제 #3
0
def check_travis_and_circleci_tests(current_branch_name):
    """Checks if all travis and circleci tests are passing on release branch.

    Args:
        current_branch_name: str. The name of current branch.

    Raises:
        Exception: The latest commit on release branch locally does not match
            the latest commit on local fork or upstream.
        Exception: The travis or circleci tests are failing.
    """
    local_sha = subprocess.check_output(
        ['git', 'rev-parse', current_branch_name])
    origin_sha = subprocess.check_output(
        ['git', 'rev-parse',
         'origin/%s' % current_branch_name])
    upstream_sha = subprocess.check_output([
        'git', 'rev-parse',
        '%s/%s' % (common.get_remote_alias(
            release_constants.REMOTE_URL), current_branch_name)
    ])
    if local_sha != origin_sha:
        raise Exception('The latest commit on release branch locally does '
                        'not match the latest commit on your local fork.')
    if local_sha != upstream_sha:
        raise Exception('The latest commit on release branch locally does '
                        'not match the latest commit on Oppia repo.')

    python_utils.PRINT('\nEnter your GitHub username.\n')
    github_username = python_utils.INPUT().lower()

    travis_url = 'https://travis-ci.org/%s/oppia/branches' % github_username
    circleci_url = 'https://circleci.com/gh/%s/workflows/oppia' % (
        github_username)

    try:
        python_utils.url_open(travis_url)
    except Exception:
        travis_url = 'https://travis-ci.org/oppia/oppia/branches'

    try:
        python_utils.url_open(circleci_url)
    except Exception:
        circleci_url = 'https://circleci.com/gh/oppia/workflows/oppia'

    common.open_new_tab_in_browser_if_possible(travis_url)
    python_utils.PRINT('Are all travis tests passing on branch %s?\n' %
                       current_branch_name)
    travis_tests_passing = python_utils.INPUT().lower()
    if travis_tests_passing not in release_constants.AFFIRMATIVE_CONFIRMATIONS:
        raise Exception('Please fix the travis tests before deploying.')

    common.open_new_tab_in_browser_if_possible(circleci_url)
    python_utils.PRINT('Are all circleci tests passing on branch %s?\n' %
                       current_branch_name)
    circleci_tests_passing = python_utils.INPUT().lower()
    if circleci_tests_passing not in (
            release_constants.AFFIRMATIVE_CONFIRMATIONS):
        raise Exception('Please fix the circleci tests before deploying.')
예제 #4
0
def post_to_mailgun(data):
    """Send POST HTTP request to mailgun api. This method is adopted from
    the requests library's post method.

    Args:
        - data: dict. The data to be sent in the request's body.

    Returns:
         Response from the server. The object is a file-like object.
         https://docs.python.org/2/library/urllib2.html
    """
    if not feconf.MAILGUN_API_KEY:
        raise Exception('Mailgun API key is not available.')

    if not feconf.MAILGUN_DOMAIN_NAME:
        raise Exception('Mailgun domain name is not set.')

    encoded = base64.b64encode(b'api:%s' % feconf.MAILGUN_API_KEY).strip()
    auth_str = 'Basic %s' % encoded
    header = {'Authorization': auth_str}
    server = (
        'https://api.mailgun.net/v3/%s/messages' % feconf.MAILGUN_DOMAIN_NAME)
    data = python_utils.url_encode(data)
    req = python_utils.url_request(server, data, header)
    return python_utils.url_open(req)
예제 #5
0
def get_chrome_driver_version():
    """Fetches the latest supported version of chromedriver depending on the
    Chrome version.
    This method follows the steps mentioned here:
    https://chromedriver.chromium.org/downloads/version-selection
    """
    try:
        proc = subprocess.Popen(
            ['google-chrome', '--version'], stdout=subprocess.PIPE)
        output = proc.stdout.readline()
    except OSError:
        raise Exception(
            'Failed to execute "google-chrome --version" command. This is '
            'used to determine the chromedriver version to use. Please set '
            'the chromedriver version manually using --chrome_driver_version '
            'flag. To determine the chromedriver version to be used, please '
            'follow the instructions mentioned in the following URL:\n'
            'https://chromedriver.chromium.org/downloads/version-selection')
    chrome_version = ''.join(re.findall(r'([0-9]|\.)', output))
    chrome_version = '.'.join(chrome_version.split('.')[:-1])
    response = python_utils.url_open(
        'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s'
        % chrome_version)
    chrome_driver_version = response.read()
    python_utils.PRINT('\n\nCHROME VERSION: %s' % chrome_version)
    return chrome_driver_version
예제 #6
0
def main(release_dir_path, deploy_data_path, personal_access_token,
         prompt_for_mailgun_and_terms_update):
    """Updates the files corresponding to LOCAL_FECONF_PATH and
    LOCAL_CONSTANTS_PATH after doing the prerequisite checks.

    Args:
        release_dir_path: str. Path of directory where all files are copied
            for release.
        deploy_data_path: str. Path for deploy data directory.
        personal_access_token: str. The personal access token for the
            GitHub id of user.
        prompt_for_mailgun_and_terms_update: bool. Whether to update mailgun api
            and last updated time for terms page.
    """
    # Do prerequisite checks.
    feconf_config_path = os.path.join(deploy_data_path,
                                      'feconf_updates.config')
    constants_config_path = os.path.join(deploy_data_path,
                                         'constants_updates.config')

    release_feconf_path = os.path.join(release_dir_path, common.FECONF_PATH)
    release_constants_path = os.path.join(release_dir_path,
                                          common.CONSTANTS_FILE_PATH)

    if prompt_for_mailgun_and_terms_update:
        try:
            python_utils.url_open(TERMS_PAGE_URL)
        except Exception:
            raise Exception('Terms mainpage does not exist on Github.')
        add_mailgun_api_key(release_feconf_path)
        check_updates_to_terms_of_service(release_feconf_path,
                                          personal_access_token)

    apply_changes_based_on_config(release_feconf_path, feconf_config_path,
                                  FECONF_REGEX)
    apply_changes_based_on_config(release_constants_path,
                                  constants_config_path, CONSTANTS_REGEX)

    common.ask_user_to_confirm(
        'Done! Please check %s and %s to ensure that '
        'the changes made are correct. Specifically verify that the '
        'MAILGUN_API_KEY and REDISHOST are updated correctly and '
        'other config changes are corresponding to %s and %s.\n' %
        (release_feconf_path, release_constants_path, feconf_config_path,
         constants_config_path))
예제 #7
0
def main(release_dir_path, deploy_data_path, personal_access_token,
         prompt_for_mailgun_and_terms_update):
    """Updates the files corresponding to LOCAL_FECONF_PATH and
    LOCAL_CONSTANTS_PATH after doing the prerequisite checks.

    Args:
        release_dir_path: str. Path of directory where all files are copied
            for release.
        deploy_data_path: str. Path for deploy data directory.
        personal_access_token: str. The personal access token for the
            GitHub id of user.
        prompt_for_mailgun_and_terms_update: bool. Whether to update mailgun api
            and last updated time for terms page.
    """
    # Do prerequisite checks.
    feconf_config_path = os.path.join(deploy_data_path,
                                      'feconf_updates.config')
    constants_config_path = os.path.join(deploy_data_path,
                                         'constants_updates.config')

    release_feconf_path = os.path.join(release_dir_path, common.FECONF_PATH)
    release_constants_path = os.path.join(release_dir_path,
                                          common.CONSTANTS_FILE_PATH)

    if prompt_for_mailgun_and_terms_update:
        try:
            python_utils.url_open(TERMS_PAGE_FOLDER_URL)
        except Exception:
            raise Exception('Terms mainpage does not exist on Github.')
        add_mailgun_api_key(release_feconf_path)
        add_mailchimp_api_key(release_feconf_path)
        check_updates_to_terms_of_service(release_feconf_path,
                                          personal_access_token)

    apply_changes_based_on_config(release_feconf_path, feconf_config_path,
                                  FECONF_REGEX)
    apply_changes_based_on_config(release_constants_path,
                                  constants_config_path, CONSTANTS_REGEX)
    verify_feconf(release_feconf_path, prompt_for_mailgun_and_terms_update)
예제 #8
0
def get_chrome_driver_version():
    """Fetches the latest supported version of chromedriver depending on the
    Chrome version.
    This method follows the steps mentioned here:
    https://chromedriver.chromium.org/downloads/version-selection
    """
    output = os.popen('google-chrome --version').read()
    chrome_version = ''.join(re.findall(r'([0-9]|\.)', output))
    chrome_version = '.'.join(chrome_version.split('.')[:-1])
    response = python_utils.url_open(
        'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s' %
        chrome_version)
    return response.read()
예제 #9
0
def download_and_unzip_files(
        source_url, target_parent_dir, zip_root_name, target_root_name):
    """Downloads a zip file, unzips it, and saves the result in a given dir.

    The download occurs only if the target directory that the zip file unzips
    to does not exist.

    NB: This function assumes that the root level of the zip file has exactly
    one folder.

    Args:
        source_url: str. The URL from which to download the zip file.
        target_parent_dir: str. The directory to save the contents of the zip
            file to.
        zip_root_name: str. The name of the top-level folder in the zip
            directory.
        target_root_name: str. The name that the top-level folder should be
            renamed to in the local directory.
    """
    if not os.path.exists(os.path.join(target_parent_dir, target_root_name)):
        python_utils.PRINT('Downloading and unzipping file %s to %s ...' % (
            zip_root_name, target_parent_dir))
        common.ensure_directory_exists(target_parent_dir)

        python_utils.url_retrieve(source_url, filename=TMP_UNZIP_PATH)

        try:
            with zipfile.ZipFile(TMP_UNZIP_PATH, 'r') as zfile:
                zfile.extractall(path=target_parent_dir)
            os.remove(TMP_UNZIP_PATH)
        except Exception:
            if os.path.exists(TMP_UNZIP_PATH):
                os.remove(TMP_UNZIP_PATH)

            # Some downloads (like jqueryui-themes) may require a user-agent.
            req = python_utils.url_request(source_url, None, {})
            req.add_header('User-agent', 'python')
            # This is needed to get a seekable filestream that can be used
            # by zipfile.ZipFile.
            file_stream = python_utils.string_io(
                buffer_value=python_utils.url_open(req).read())
            with zipfile.ZipFile(file_stream, 'r') as zfile:
                zfile.extractall(path=target_parent_dir)

        # Rename the target directory.
        os.rename(
            os.path.join(target_parent_dir, zip_root_name),
            os.path.join(target_parent_dir, target_root_name))

        python_utils.PRINT('Download of %s succeeded.' % zip_root_name)
def verify_target_version_compatible_with_latest_release(
        target_version):
    """Checks that the target version is consistent with the latest released
    version on GitHub.

    Args:
        target_version: str. The release version.

    Raises:
        Exception. Failed to fetch latest release info from GitHub.
        Exception. Could not parse version number of latest GitHub release.
        AssertionError. The previous and the current major version are not the
            same.
        AssertionError. The current patch version is not equal to previous patch
            version plus one.
        AssertionError. The current patch version is greater or equal to 10.
        AssertionError. The current minor version is not equal to previous
            minor version plus one.
        AssertionError. The current patch version is different than 0.
    """
    response = python_utils.url_open(
        'https://api.github.com/repos/oppia/oppia/releases/latest')
    if response.getcode() != 200:
        raise Exception(
            'ERROR: Failed to fetch latest release info from GitHub.')

    data = json.load(response)
    latest_release_tag_name = data['tag_name']

    match_result = re.match(r'v(\d)\.(\d)\.(\d)', latest_release_tag_name)
    if match_result is None:
        raise Exception(
            'ERROR: Could not parse version number of latest GitHub release.')
    prev_major, prev_minor, prev_patch = match_result.group(1, 2, 3)

    match_result = re.match(r'(\d)\.(\d)\.(\d)', target_version)
    curr_major, curr_minor, curr_patch = match_result.group(1, 2, 3)

    # This will need to be overridden if the major version changes.
    assert prev_major == curr_major, 'Unexpected major version change.'
    if prev_minor == curr_minor:
        assert int(curr_patch) == int(prev_patch) + 1, (
            'The current patch version is not equal to previous patch '
            'version plus one.')
    else:
        assert int(curr_minor) == int(prev_minor) + 1, (
            'The current minor version is not equal to previous '
            'minor version plus one.')
        assert int(curr_patch) == 0, (
            'The current patch version is different than 0.')
예제 #11
0
def main(personal_access_token):
    """Updates the files corresponding to LOCAL_FECONF_PATH and
    LOCAL_CONSTANTS_PATH after doing the prerequisite checks.

    Args:
        personal_access_token: str. The personal access token for the
            GitHub id of user.
    """
    # Do prerequisite checks.
    common.require_cwd_to_be_oppia()
    assert common.is_current_branch_a_release_branch(), (
        'Current branch is not a release branch_name')
    common.ensure_release_scripts_folder_exists_and_is_up_to_date()
    try:
        python_utils.url_open(TERMS_PAGE_URL)
    except Exception:
        raise Exception('Terms mainpage does not exist on Github.')

    try:
        check_updates_to_terms_of_service(personal_access_token)
        add_mailgun_api_key()

        apply_changes_based_on_config(
            LOCAL_FECONF_PATH, FECONF_CONFIG_PATH, FECONF_REGEX)
        apply_changes_based_on_config(
            LOCAL_CONSTANTS_PATH, CONSTANTS_CONFIG_PATH, CONSTANTS_REGEX)
    except Exception as e:
        common.run_cmd([
            'git', 'checkout', '--', LOCAL_FECONF_PATH, LOCAL_CONSTANTS_PATH])
        raise Exception(e)

    common.ask_user_to_confirm(
        'Done! Please check feconf.py and assets/constants.ts to ensure that '
        'the changes made are correct. Specifically verify that the '
        'MAILGUN_API_KEY and REDISHOST are updated correctly and '
        'other config changes are corresponding to %s and %s.\n' % (
            FECONF_CONFIG_PATH, CONSTANTS_CONFIG_PATH))
예제 #12
0
def get_chrome_driver_version():
    """Fetches the latest supported version of chromedriver depending on the
    Chrome version.
    This method follows the steps mentioned here:
    https://chromedriver.chromium.org/downloads/version-selection
    """
    popen_args = ['google-chrome', '--version']
    if common.is_mac_os():
        # There are spaces between Google and Chrome in the path. Spaces don't
        # need to be escaped when we're not using the terminal, ie. shell=False
        # for Popen by default.
        popen_args = [
            '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
            '--version'
        ]
    try:
        proc = subprocess.Popen(popen_args, stdout=subprocess.PIPE)
        output = proc.stdout.readline()
    except OSError:
        # For the error message for the mac command, we need to add the
        # backslashes in. This is because it is likely that a user will try to
        # run the command on their terminal and, as mentioned above, the mac
        # get chrome version command has spaces in the path which need to be
        # escaped for successful terminal use.
        raise Exception(
            'Failed to execute "%s" command. '
            'This is used to determine the chromedriver version to use. '
            'Please set the chromedriver version manually using '
            '--chrome_driver_version flag. To determine the chromedriver '
            'version to be used, please follow the instructions mentioned '
            'in the following URL:\n'
            'https://chromedriver.chromium.org/downloads/version-selection' % (
                ' '.join(arg.replace(' ', r'\ ') for arg in popen_args)
            )
        )
    chrome_version = ''.join(re.findall(r'([0-9]|\.)', output))
    chrome_version = '.'.join(chrome_version.split('.')[:-1])
    response = python_utils.url_open(
        'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s'
        % chrome_version)
    chrome_driver_version = response.read()
    python_utils.PRINT('\n\nCHROME VERSION: %s' % chrome_version)
    return chrome_driver_version
예제 #13
0
def lookup_pr(owner, repo, pull_number):
    """Lookup a PR using the GitHub API.

    Args:
        owner: str. Owner of the repository the PR is in.
        repo: str. Repository the PR is in.
        pull_number: str. PR number.

    Returns:
        dict. JSON object returned by the GitHub API v3. This is an
        empty dictionary if the response code from the GitHub API is not
        200.
    """
    request = python_utils.url_request(
        GITHUB_API_PR_ENDPOINT % (owner, repo, pull_number), None,
        {'Accept': 'application/vnd.github.v3+json'})
    response = python_utils.url_open(request)
    if response.getcode() != 200:
        return {}
    pr = json.load(response)
    response.close()
    return pr
예제 #14
0
def send_email_to_recipients(sender_email,
                             recipient_emails,
                             subject,
                             plaintext_body,
                             html_body,
                             bcc=None,
                             reply_to=None,
                             recipient_variables=None):
    """Send POST HTTP request to mailgun api. This method is adopted from
    the requests library's post method.

    Args:
        sender_email: str. The email address of the sender. This should be in
            the form 'SENDER_NAME <SENDER_EMAIL_ADDRESS>' or
            'SENDER_EMAIL_ADDRESS'. Must be utf-8.
        recipient_emails: list(str). The email addresses of the recipients.
            Must be utf-8.
        subject: str. The subject line of the email, Must be utf-8.
        plaintext_body: str. The plaintext body of the email. Must be utf-8.
        html_body: str. The HTML body of the email. Must fit in a datastore
            entity. Must be utf-8.
        bcc: list(str)|None. Optional argument. List of bcc emails.
        reply_to: str|None. Optional argument. Reply address formatted like
            “reply+<reply_id>@<incoming_email_domain_name>
            reply_id is the unique id of the sender.
        recipient_variables: dict|None. Optional argument. If batch sending
            requires differentiating each email based on the recipient, we
            assign a unique id to each recipient, including info relevant to
            that recipient so that we can reference it when composing the
            email like so:
                recipient_variables =
                    {"*****@*****.**": {"first":"Bob", "id":1},
                     "*****@*****.**": {"first":"Alice", "id":2}}
                subject = 'Hey, %recipient.first%’
            More info about this format at:
            https://documentation.mailgun.com/en/
                latest/user_manual.html#batch-sending.

    Raises:
        Exception. The mailgun api key is not stored in
            feconf.MAILGUN_API_KEY.
        Exception. The mailgun domain name is not stored in
            feconf.MAILGUN_DOMAIN_NAME.

    Returns:
        bool. Whether the emails are sent successfully.
    """
    if not feconf.MAILGUN_API_KEY:
        raise Exception('Mailgun API key is not available.')

    if not feconf.MAILGUN_DOMAIN_NAME:
        raise Exception('Mailgun domain name is not set.')

    # To send bulk emails we pass list of recipients in 'to' paarameter of
    # post data. Maximum limit of recipients per request is 1000.
    # For more detail check following link:
    # https://documentation.mailgun.com/user_manual.html#batch-sending
    recipient_email_lists = [
        recipient_emails[i:i + 1000]
        for i in python_utils.RANGE(0, len(recipient_emails), 1000)
    ]
    for email_list in recipient_email_lists:
        data = {
            'from': sender_email,
            'subject': subject,
            'text': plaintext_body,
            'html': html_body
        }

        data['to'] = email_list[0] if len(email_list) == 1 else email_list

        if bcc:
            data['bcc'] = bcc[0] if len(bcc) == 1 else bcc

        if reply_to:
            data['h:Reply-To'] = reply_to

        # 'recipient-variable' in post data forces mailgun to send individual
        # email to each recipient (This is intended to be a workaround for
        # sending individual emails).
        data['recipient_variables'] = recipient_variables or {}

        encoded = base64.b64encode(b'api:%s' % feconf.MAILGUN_API_KEY).strip()
        auth_str = 'Basic %s' % encoded
        header = {'Authorization': auth_str}
        server = (('https://api.mailgun.net/v3/%s/messages') %
                  feconf.MAILGUN_DOMAIN_NAME)
        encoded_url = python_utils.url_encode(data)
        req = python_utils.url_request(server, encoded_url, header)
        resp = python_utils.url_open(req)
        # The function url_open returns a file_like object that can be queried
        # for the status code of the url query. If it is not 200, the mail query
        # failed so we return False (this function did not complete
        # successfully).
        if resp.getcode() != 200:
            return False
    return True
예제 #15
0
 def test_url_open(self):
     response = python_utils.url_open('http://www.google.com')
     self.assertEqual(response.getcode(), 200)
     self.assertEqual(response.url, 'http://www.google.com')
예제 #16
0
def managed_webdriver_server(chrome_version=None):
    """Returns context manager to start/stop the Webdriver server gracefully.

    This context manager updates Google Chrome before starting the server.

    Args:
        chrome_version: str|None. The version of Google Chrome to run the tests
            on. If None, then the currently-installed version of Google Chrome
            is used instead.

    Yields:
        psutil.Process. The Webdriver process.
    """
    if chrome_version is None:
        # Although there are spaces between Google and Chrome in the path, we
        # don't need to escape them for Popen (as opposed to on the terminal, in
        # which case we would need to escape them for the command to run).
        chrome_command = (
            '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
            if common.is_mac_os() else 'google-chrome')
        try:
            output = subprocess.check_output([chrome_command, '--version'])
        except OSError:
            # For the error message on macOS, we need to add the backslashes in.
            # This is because it is likely that a user will try to run the
            # command on their terminal and, as mentioned above, the macOS
            # chrome version command has spaces in the path which need to be
            # escaped for successful terminal use.
            raise Exception(
                'Failed to execute "%s --version" command. This is used to '
                'determine the chromedriver version to use. Please set the '
                'chromedriver version manually using --chrome_driver_version '
                'flag. To determine the chromedriver version to be used, '
                'please follow the instructions mentioned in the following '
                'URL:\n'
                'https://chromedriver.chromium.org/downloads/version-selection'
                % chrome_command.replace(' ', r'\ '))

        installed_version_parts = b''.join(re.findall(rb'[0-9.]', output))
        installed_version = '.'.join(
            installed_version_parts.decode('utf-8').split('.')[:-1])
        response = python_utils.url_open(
            'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s' %
            (installed_version))
        chrome_version = response.read().decode('utf-8')

    python_utils.PRINT('\n\nCHROME VERSION: %s' % chrome_version)
    subprocess.check_call([
        common.NODE_BIN_PATH,
        common.WEBDRIVER_MANAGER_BIN_PATH,
        'update',
        '--versions.chrome',
        chrome_version,
    ])

    with python_utils.ExitStack() as exit_stack:
        if common.is_windows_os():
            # NOTE: webdriver-manager (version 13.0.0) uses `os.arch()` to
            # determine the architecture of the operating system, however, this
            # function can only be used to determine the architecture of the
            # machine that compiled `node`. In the case of Windows, we are using
            # the portable version, which was compiled on `ia32` machine so that
            # is the value returned by this `os.arch` function. Unfortunately,
            # webdriver-manager seems to assume that Windows wouldn't run on the
            # ia32 architecture, so its help function used to determine download
            # link returns null for this, which means that the application has
            # no idea about where to download the correct version.
            #
            # https://github.com/angular/webdriver-manager/blob/b7539a5a3897a8a76abae7245f0de8175718b142/lib/provider/chromedriver.ts#L16
            # https://github.com/angular/webdriver-manager/blob/b7539a5a3897a8a76abae7245f0de8175718b142/lib/provider/geckodriver.ts#L21
            # https://github.com/angular/webdriver-manager/blob/b7539a5a3897a8a76abae7245f0de8175718b142/lib/provider/chromedriver.ts#L167
            # https://github.com/nodejs/node/issues/17036
            regex_pattern = re.escape('this.osArch = os.arch();')
            arch = 'x64' if common.is_x64_architecture() else 'x86'
            replacement_string = 'this.osArch = "%s";' % arch
            exit_stack.enter_context(
                common.inplace_replace_file_context(
                    common.CHROME_PROVIDER_FILE_PATH, regex_pattern,
                    replacement_string))
            exit_stack.enter_context(
                common.inplace_replace_file_context(
                    common.GECKO_PROVIDER_FILE_PATH, regex_pattern,
                    replacement_string))

        # OK to use shell=True here because we are passing string literals and
        # constants, so there is no risk of a shell-injection attack.
        proc = exit_stack.enter_context(
            managed_process([
                common.NODE_BIN_PATH,
                common.WEBDRIVER_MANAGER_BIN_PATH,
                'start',
                '--versions.chrome',
                chrome_version,
                '--quiet',
                '--standalone',
            ],
                            human_readable_name='Webdriver manager',
                            shell=True))

        common.wait_for_port_to_be_in_use(4444)

        yield proc