Remove the passed package(s) from the system using winrepo

    .. versionadded:: 0.16.0

    Args:
        name (str):
            The name(s) of the package(s) to be uninstalled. Can be a
            single package or a comma delimited list of packages, no spaces.

        pkgs (list):
            A list of packages to delete. Must be passed as a python list. The
            ``name`` parameter will be ignored if this option is passed.

    Kwargs:

        version (str):
            The version of the package to be uninstalled. If this option is
            used to to uninstall multiple packages, then this version will be
            applied to all targeted packages. Recommended using only when
            uninstalling a single package. If this parameter is omitted, the
            latest version will be uninstalled.

        saltenv (str): Salt environment. Default ``base``
        refresh (bool): Refresh package metadata. Default ``False``

    Returns:
        dict: Returns a dict containing the changes.

        If the package is removed by ``pkg.remove``:

            {'<package>': {'old': '<old-version>',
                           'new': '<new-version>'}}

        If the package is already uninstalled:

            {'<package>': {'current': 'not installed'}}

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.remove <package name>
        salt '*' pkg.remove <package1>,<package2>,<package3>
        salt '*' pkg.remove pkgs='["foo", "bar"]'

def remove(name=None, pkgs=None, **kwargs):
    '''
    Remove the passed package(s) from the system using winrepo

    .. versionadded:: 0.16.0

    Args:
        name (str):
            The name(s) of the package(s) to be uninstalled. Can be a
            single package or a comma delimited list of packages, no spaces.

        pkgs (list):
            A list of packages to delete. Must be passed as a python list. The
            ``name`` parameter will be ignored if this option is passed.

    Kwargs:

        version (str):
            The version of the package to be uninstalled. If this option is
            used to to uninstall multiple packages, then this version will be
            applied to all targeted packages. Recommended using only when
            uninstalling a single package. If this parameter is omitted, the
            latest version will be uninstalled.

        saltenv (str): Salt environment. Default ``base``
        refresh (bool): Refresh package metadata. Default ``False``

    Returns:
        dict: Returns a dict containing the changes.

        If the package is removed by ``pkg.remove``:

            {'<package>': {'old': '<old-version>',
                           'new': '<new-version>'}}

        If the package is already uninstalled:

            {'<package>': {'current': 'not installed'}}

    CLI Example:

    .. code-block:: bash

        salt '*' pkg.remove <package name>
        salt '*' pkg.remove <package1>,<package2>,<package3>
        salt '*' pkg.remove pkgs='["foo", "bar"]'
    '''
    saltenv = kwargs.get('saltenv', 'base')
    refresh = salt.utils.data.is_true(kwargs.get('refresh', False))
    # no need to call _refresh_db_conditional as list_pkgs will do it
    ret = {}

    # Make sure name or pkgs is passed
    if not name and not pkgs:
        return 'Must pass a single package or a list of packages'

    # Get package parameters
    pkg_params = __salt__['pkg_resource.parse_targets'](name, pkgs, **kwargs)[0]

    # Get a list of currently installed software for comparison at the end
    old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True)

    # Loop through each package
    changed = []  # list of changed package names
    for pkgname, version_num in six.iteritems(pkg_params):

        # Load package information for the package
        pkginfo = _get_package_info(pkgname, saltenv=saltenv)

        # Make sure pkginfo was found
        if not pkginfo:
            msg = 'Unable to locate package {0}'.format(pkgname)
            log.error(msg)
            ret[pkgname] = msg
            continue

        # Check to see if package is installed on the system
        if pkgname not in old:
            log.debug('%s %s not installed', pkgname, version_num if version_num else '')
            ret[pkgname] = {'current': 'not installed'}
            continue

        removal_targets = []
        # Only support a single version number
        if version_num is not None:
            #  Using the salt cmdline with version=5.3 might be interpreted
            #  as a float it must be converted to a string in order for
            #  string matching to work.
            version_num = six.text_type(version_num)

        # At least one version of the software is installed.
        if version_num is None:
            for ver_install in old[pkgname]:
                if ver_install not in pkginfo and 'latest' in pkginfo:
                    log.debug('%s %s using package latest entry to to remove', pkgname, version_num)
                    removal_targets.append('latest')
                else:
                    removal_targets.append(ver_install)
        else:
            if version_num in pkginfo:
                # we known how to remove this version
                if version_num in old[pkgname]:
                    removal_targets.append(version_num)
                else:
                    log.debug('%s %s not installed', pkgname, version_num)
                    ret[pkgname] = {'current': '{0} not installed'.format(version_num)}
                    continue
            elif 'latest' in pkginfo:
                # we do not have version entry, assume software can self upgrade and use latest
                log.debug('%s %s using package latest entry to to remove', pkgname, version_num)
                removal_targets.append('latest')

        if not removal_targets:
            log.error('%s %s no definition to remove this version', pkgname, version_num)
            ret[pkgname] = {
                    'current': '{0} no definition, cannot removed'.format(version_num)
                }
            continue

        for target in removal_targets:
            # Get the uninstaller
            uninstaller = pkginfo[target].get('uninstaller', '')
            cache_dir = pkginfo[target].get('cache_dir', False)
            uninstall_flags = pkginfo[target].get('uninstall_flags', '')

            # If no uninstaller found, use the installer with uninstall flags
            if not uninstaller and uninstall_flags:
                uninstaller = pkginfo[target].get('installer', '')

            # If still no uninstaller found, fail
            if not uninstaller:
                log.error(
                    'No installer or uninstaller configured for package %s',
                    pkgname,
                )
                ret[pkgname] = {'no uninstaller defined': target}
                continue

            # Where is the uninstaller
            if uninstaller.startswith(('salt:', 'http:', 'https:', 'ftp:')):

                # Check for the 'cache_dir' parameter in the .sls file
                # If true, the entire directory will be cached instead of the
                # individual file. This is useful for installations that are not
                # single files

                if cache_dir and uninstaller.startswith('salt:'):
                    path, _ = os.path.split(uninstaller)
                    __salt__['cp.cache_dir'](path,
                                             saltenv,
                                             False,
                                             None,
                                             'E@init.sls$')

                # Check to see if the uninstaller is cached
                cached_pkg = __salt__['cp.is_cached'](uninstaller, saltenv)
                if not cached_pkg:
                    # It's not cached. Cache it, mate.
                    cached_pkg = __salt__['cp.cache_file'](uninstaller, saltenv)

                    # Check if the uninstaller was cached successfully
                    if not cached_pkg:
                        log.error('Unable to cache %s', uninstaller)
                        ret[pkgname] = {'unable to cache': uninstaller}
                        continue

                # Compare the hash of the cached installer to the source only if
                # the file is hosted on salt:
                # TODO cp.cache_file does cache and hash checking? So why do it again?
                if uninstaller.startswith('salt:'):
                    if __salt__['cp.hash_file'](uninstaller, saltenv) != \
                            __salt__['cp.hash_file'](cached_pkg):
                        try:
                            cached_pkg = __salt__['cp.cache_file'](
                                uninstaller, saltenv)
                        except MinionError as exc:
                            return '{0}: {1}'.format(exc, uninstaller)

                        # Check if the installer was cached successfully
                        if not cached_pkg:
                            log.error('Unable to cache %s', uninstaller)
                            ret[pkgname] = {'unable to cache': uninstaller}
                            continue
            else:
                # Run the uninstaller directly
                # (not hosted on salt:, https:, etc.)
                cached_pkg = os.path.expandvars(uninstaller)

            # Fix non-windows slashes
            cached_pkg = cached_pkg.replace('/', '\\')
            cache_path, _ = os.path.split(cached_pkg)

            # os.path.expandvars is not required as we run everything through cmd.exe /s /c

            if kwargs.get('extra_uninstall_flags'):
                uninstall_flags = '{0} {1}'.format(
                    uninstall_flags, kwargs.get('extra_uninstall_flags', ''))

            # Compute msiexec string
            use_msiexec, msiexec = _get_msiexec(pkginfo[target].get('msiexec', False))
            cmd_shell = os.getenv('ComSpec', '{0}\\system32\\cmd.exe'.format(os.getenv('WINDIR')))

            # Build cmd and arguments
            # cmd and arguments must be separated for use with the task scheduler
            if use_msiexec:
                # Check if uninstaller is set to {guid}, if not we assume its a remote msi file.
                # which has already been downloaded.
                arguments = '"{0}" /X "{1}"'.format(msiexec, cached_pkg)
            else:
                arguments = '"{0}"'.format(cached_pkg)

            if uninstall_flags:
                arguments = '{0} {1}'.format(arguments, uninstall_flags)

            # Uninstall the software
            changed.append(pkgname)
            # Check Use Scheduler Option
            if pkginfo[target].get('use_scheduler', False):
                # Create Scheduled Task
                __salt__['task.create_task'](name='update-salt-software',
                                             user_name='System',
                                             force=True,
                                             action_type='Execute',
                                             cmd=cmd_shell,
                                             arguments='/s /c "{0}"'.format(arguments),
                                             start_in=cache_path,
                                             trigger_type='Once',
                                             start_date='1975-01-01',
                                             start_time='01:00',
                                             ac_only=False,
                                             stop_if_on_batteries=False)
                # Run Scheduled Task
                if not __salt__['task.run_wait'](name='update-salt-software'):
                    log.error('Failed to remove %s', pkgname)
                    log.error('Scheduled Task failed to run')
                    ret[pkgname] = {'uninstall status': 'failed'}
            else:
                # Launch the command
                result = __salt__['cmd.run_all'](
                        '"{0}" /s /c "{1}"'.format(cmd_shell, arguments),
                        output_loglevel='trace',
                        python_shell=False,
                        redirect_stderr=True)
                if not result['retcode']:
                    ret[pkgname] = {'uninstall status': 'success'}
                    changed.append(pkgname)
                elif result['retcode'] == 3010:
                    # 3010 is ERROR_SUCCESS_REBOOT_REQUIRED
                    report_reboot_exit_codes = kwargs.pop(
                        'report_reboot_exit_codes', True)
                    if report_reboot_exit_codes:
                        __salt__['system.set_reboot_required_witnessed']()
                    ret[pkgname] = {'uninstall status': 'success, reboot required'}
                    changed.append(pkgname)
                elif result['retcode'] == 1641:
                    # 1641 is ERROR_SUCCESS_REBOOT_INITIATED
                    ret[pkgname] = {'uninstall status': 'success, reboot initiated'}
                    changed.append(pkgname)
                else:
                    log.error('Failed to remove %s', pkgname)
                    log.error('retcode %s', result['retcode'])
                    log.error('uninstaller output: %s', result['stdout'])
                    ret[pkgname] = {'uninstall status': 'failed'}

    # Get a new list of installed software
    new = list_pkgs(saltenv=saltenv, refresh=False)

    # Take the "old" package list and convert the values to strings in
    # preparation for the comparison below.
    __salt__['pkg_resource.stringify'](old)

    # Check for changes in the registry
    difference = salt.utils.data.compare_dicts(old, new)
    found_chgs = all(name in difference for name in changed)
    end_t = time.time() + 3  # give it 3 seconds to catch up.
    while not found_chgs and time.time() < end_t:
        time.sleep(0.5)
        new = list_pkgs(saltenv=saltenv, refresh=False)
        difference = salt.utils.data.compare_dicts(old, new)
        found_chgs = all(name in difference for name in changed)

    if not found_chgs:
        log.warning('Expected changes for package removal may not have occured')

    # Compare the software list before and after
    # Add the difference to ret
    ret.update(difference)
    return ret