WizardKit/scripts/wk/setup/win.py

406 lines
11 KiB
Python

"""WizardKit: Setup - Windows"""
# vim: sts=2 sw=2 ts=2
import logging
import os
import platform
import re
import sys
import time
from subprocess import CalledProcessError, DEVNULL
from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT
from wk.exe import (
get_procs,
run_program,
popen_program,
wait_for_procs,
)
from wk.io import (
delete_folder,
get_path_obj,
non_clobber_path,
rename_item,
)
from wk.kit.tools import (
ARCH,
download_tool,
extract_tool,
find_kit_dir,
get_tool_path,
run_tool,
)
from wk.log import format_log_path, update_log_path
from wk.os.win import (
reg_delete_value,
reg_read_value,
reg_set_value,
reg_write_settings,
disable_service,
enable_service,
stop_service,
)
from wk.repairs.win import (
backup_all_browser_profiles,
backup_registry,
create_custom_power_plan,
create_system_restore_point,
enable_windows_updates,
export_power_plans,
reset_power_plans,
set_system_restore_size,
)
from wk.std import (
GenericError,
GenericWarning,
Menu,
TryAndPrint,
abort,
ask,
clear_screen,
color_string,
pause,
print_info,
print_standard,
print_warning,
set_title,
show_data,
sleep,
strip_colors,
)
# STATIC VARIABLES
LOG = logging.getLogger(__name__)
CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False)
IN_CONEMU = 'ConEmuPID' in os.environ
LIBREOFFICE_XCU_DATA = '''<?xml version="1.0" encoding="UTF-8"?>
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item oor:path="/org.openoffice.Setup/Office/Factories/org.openoffice.Setup:Factory['com.sun.star.presentation.PresentationDocument']"><prop oor:name="ooSetupFactoryDefaultFilter" oor:op="fuse"><value>Impress MS PowerPoint 2007 XML</value></prop></item>
<item oor:path="/org.openoffice.Setup/Office/Factories/org.openoffice.Setup:Factory['com.sun.star.sheet.SpreadsheetDocument']"><prop oor:name="ooSetupFactoryDefaultFilter" oor:op="fuse"><value>Calc MS Excel 2007 XML</value></prop></item>
<item oor:path="/org.openoffice.Setup/Office/Factories/org.openoffice.Setup:Factory['com.sun.star.text.TextDocument']"><prop oor:name="ooSetupFactoryDefaultFilter" oor:op="fuse"><value>MS Word 2007 XML</value></prop></item>
<item oor:path="/org.openoffice.Office.Common/Save/Document"><prop oor:name="WarnAlienFormat" oor:op="fuse"><value>false</value></prop></item>
</oor:items>
'''
MENU_PRESETS = Menu()
PROGRAMFILES_32 = os.environ.get(
'PROGRAMFILES(X86)', os.environ.get(
'PROGRAMFILES', r'C:\Program Files (x86)',
),
)
OS_VERSION = float(platform.win32_ver()[0])
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:')
WIDTH = 50
TRY_PRINT = TryAndPrint()
TRY_PRINT.width = WIDTH
TRY_PRINT.verbose = True
for error in ('CalledProcessError', 'FileNotFoundError'):
TRY_PRINT.add_error(error)
# Auto Setup
def build_menus(base_menus, title, presets):
"""Build menus, returns dict."""
menus = {}
menus['Main'] = Menu(title=f'{title}\n{color_string("Main Menu", "GREEN")}')
# Main Menu
for entry in base_menus['Actions']:
menus['Main'].add_action(entry.name, entry.details)
for group in base_menus['Groups']:
menus['Main'].add_option(group, {'Selected': True})
# Run groups
for group, entries in base_menus['Groups'].items():
menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}')
for entry in entries:
menus[group].add_option(entry.name, entry.details)
menus[group].add_action('All')
menus[group].add_action('None')
menus[group].add_action('Main Menu', {'Separator': True})
menus[group].add_action('Quit')
# Initialize main menu display names
menus['Main'].update()
# Fix Function references
for group, menu in menus.items():
if group not in base_menus['Groups']:
continue
for name in menu.options:
_function = menu.options[name]['Function']
if isinstance(_function, str):
menu.options[name]['Function'] = getattr(
sys.modules[__name__], _function,
)
# Update presets Menu
MENU_PRESETS.title = f'{title}\n{color_string("Load Preset", "GREEN")}'
MENU_PRESETS.add_option('Default')
for name in presets:
MENU_PRESETS.add_option(name)
MENU_PRESETS.add_action('Main Menu')
MENU_PRESETS.add_action('Quit')
MENU_PRESETS.update()
# Done
return menus
def load_preset(menus, presets):
"""Load menu settings from preset."""
selection = MENU_PRESETS.simple_select()
# Exit early
if 'Main Menu' in selection:
return
if 'Quit' in selection:
raise SystemExit
# Default case
if 'Default' in selection:
for menu in menus.values():
for name in menu.options:
menu.options[name]['Selected'] = True
return
# Load preset
preset = presets[selection[0]]
for group, menu in menus.items():
group_enabled = group in preset
for name in menu.options:
value = group_enabled and name in preset[group]
menu.options[name]['Selected'] = value
def run_auto_setup(base_menus, presets):
"""Run Auto Setup."""
update_log_path(dest_name='Auto Setup', timestamp=True)
title = f'{KIT_NAME_FULL}: Auto Setup'
clear_screen()
set_title(title)
print_info(title)
print('')
# Generate menus
print_standard('Initializing...')
menus = build_menus(base_menus, title, presets)
# Show Menu
show_main_menu(base_menus, menus, presets)
# Start setup
clear_screen()
print_standard(title)
print('')
print_info('Running setup')
# Run setup
for group, menu in menus.items():
if group in ('Main', 'Options'):
continue
try:
run_group(group, menu)
except KeyboardInterrupt:
abort()
# Done
print_info('Done')
pause('Press Enter to exit...')
def run_group(group, menu):
"""Run entries in group if appropriate."""
print_info(f' {group}')
for name, details in menu.options.items():
name_str = strip_colors(name)
# Not selected
if not details.get('Selected', False):
show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
continue
# Selected
details['Function']()
def show_main_menu(base_menus, menus, presets):
"""Show main menu and handle actions."""
while True:
update_main_menu(menus)
selection = menus['Main'].simple_select(update=False)
if selection[0] in base_menus['Groups'] or selection[0] == 'Options':
show_sub_menu(menus[selection[0]])
if selection[0] == 'Load Preset':
load_preset(menus, presets)
elif 'Start' in selection:
break
elif 'Quit' in selection:
raise SystemExit
def show_sub_menu(menu):
"""Show sub-menu and handle sub-menu actions."""
while True:
selection = menu.advanced_select()
if 'Main Menu' in selection:
break
if 'Quit' in selection:
raise SystemExit
# Select all or none
value = 'All' in selection
for name in menu.options:
if not menu.options[name].get('Disabled', False):
menu.options[name]['Selected'] = value
def update_main_menu(menus):
"""Update main menu based on current selections."""
index = 1
skip = 'Reboot'
for name in menus['Main'].options:
checkmark = ' '
selected = [
_v['Selected'] for _k, _v in menus[name].options.items() if _k != skip
]
if all(selected):
checkmark = ''
elif any(selected):
checkmark = '-'
display_name = f' {index}: [{checkmark}] {name}'
index += 1
menus['Main'].options[name]['Display Name'] = display_name
# Auto Repairs: Wrapper Functions
def auto_backup_registry():
"""Backup registry."""
TRY_PRINT.run('Backup Registry...', backup_registry)
def auto_backup_browser_profiles():
"""Backup browser profiles."""
backup_all_browser_profiles(use_try_print=True)
def auto_backup_power_plans():
"""Backup power plans."""
TRY_PRINT.run('Backup Power Plans...', export_power_plans)
def auto_reset_power_plans():
"""Reset power plans."""
TRY_PRINT.run('Reset Power Plans...', reset_power_plans)
def auto_set_custom_power_plan():
"""Set custom power plan."""
TRY_PRINT.run('Set Custom Power Plan...', create_custom_power_plan)
def auto_enable_regback():
"""Enable RegBack."""
TRY_PRINT.run(
'Enable RegBack...', reg_set_value, 'HKLM',
r'System\CurrentControlSet\Control\Session Manager\Configuration Manager',
'EnablePeriodicBackup', 1, 'DWORD',
)
def auto_system_restore_enable():
"""Enable System Restore."""
cmd = [
'powershell', '-Command', 'Enable-ComputerRestore',
'-Drive', SYSTEMDRIVE,
]
TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd)
def auto_system_restore_set_size():
"""Set System Restore size."""
TRY_PRINT.run('Set System Restore Size...', set_system_restore_size)
def auto_system_restore_create():
"""Create System Restore point."""
TRY_PRINT.run('Create System Restore...', create_system_restore_point)
def auto_windows_updates_enable():
"""Enable Windows Updates."""
TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)
# Auto Setup: Wrapper Functions
def auto_install_libreoffice():
"""Install LibreOffice.
NOTE: It is assumed that auto_install_vcredists() will be run
before this function to satisfy the vcredist=False usage.
"""
TRY_PRINT.run('LibreOffice...', install_libreoffice, vcredist=False)
def auto_install_vcredists():
"""Install latest supported Visual C++ runtimes."""
TRY_PRINT.run('Visual C++ Runtimes...', install_vcredists)
# Install Functions
def install_libreoffice(
register_mso_types=True, use_mso_formats=False, vcredist=True):
"""Install LibreOffice."""
installer = find_kit_dir('Installers').joinpath('LibreOffice.msi')
xcu_dir = get_path_obj(f'{os.environ.get("APPDATA")}/LibreOffice/4/user')
xcu_file = xcu_dir.joinpath('registrymodifications.xcu')
# Set default save formats to MSO types
if use_mso_formats and not xcu_file.exists():
xcu_dir.mkdir(parents=True, exist_ok=True)
with open(xcu_file, 'w', encoding='utf-8', newline='\n') as _f:
_f.write(LIBREOFFICE_XCU_DATA)
# Build cmd
cmd = [
'msiexec', '/passive', '/norestart',
'/i', installer,
'REBOOTYESNO=No',
f'VC_REDIST={1 if vcredist else 0}',
]
if register_mso_types:
cmd.append('REGISTER_ALL_MSO_TYPES=1')
else:
cmd.append('REGISTER_NO_MSO_TYPES=1')
# Install LibreOffice
run_program(cmd)
def install_vcredists():
"""Install latest supported Visual C++ runtimes."""
for year in (2012, 2013, 2019):
cmd_args = ['/install', '/passive', '/norestart']
if year == 2012:
cmd_args.pop(0)
name = f'VCRedist_{year}_x32'
download_tool('VCRedist', name)
installer = get_tool_path('VCRedist', name)
run_program([installer, *cmd_args])
if ARCH == '64':
name = f'{name[:-2]}64'
download_tool('VCRedist', name)
installer = get_tool_path('VCRedist', name)
run_program([installer, *cmd_args])
# Misc Functions
## TODO?
# Tool Functions
## TODO?
# OS Built-in Functions
## TODO?
if __name__ == '__main__':
print("This file is not meant to be called directly.")