diff options
Diffstat (limited to 'Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp')
-rw-r--r-- | Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp | 3132 |
1 files changed, 3132 insertions, 0 deletions
diff --git a/Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp b/Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp new file mode 100644 index 0000000..416f2d5 --- /dev/null +++ b/Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp @@ -0,0 +1,3132 @@ +//------------------------------------------------------------------------------------------------- +// <copyright file="WixStandardBootstrapperApplication.cpp" company="Outercurve Foundation"> +// Copyright (c) 2004, Outercurve Foundation. +// This software is released under Microsoft Reciprocal License (MS-RL). +// The license and further copyright text can be found in the file +// LICENSE.TXT at the root directory of the distribution. +// </copyright> +//------------------------------------------------------------------------------------------------- + + +#include "pch.h" + +static const LPCWSTR PYBA_WINDOW_CLASS = L"PythonBA"; +static const LPCWSTR PYBA_VARIABLE_LAUNCH_TARGET_PATH = L"LaunchTarget"; +static const LPCWSTR PYBA_VARIABLE_LAUNCH_TARGET_ELEVATED_ID = L"LaunchTargetElevatedId"; +static const LPCWSTR PYBA_VARIABLE_LAUNCH_ARGUMENTS = L"LaunchArguments"; +static const LPCWSTR PYBA_VARIABLE_LAUNCH_HIDDEN = L"LaunchHidden"; +static const DWORD PYBA_ACQUIRE_PERCENTAGE = 30; +static const LPCWSTR PYBA_VARIABLE_BUNDLE_FILE_VERSION = L"WixBundleFileVersion"; + +enum PYBA_STATE { + PYBA_STATE_INITIALIZING, + PYBA_STATE_INITIALIZED, + PYBA_STATE_HELP, + PYBA_STATE_DETECTING, + PYBA_STATE_DETECTED, + PYBA_STATE_PLANNING, + PYBA_STATE_PLANNED, + PYBA_STATE_APPLYING, + PYBA_STATE_CACHING, + PYBA_STATE_CACHED, + PYBA_STATE_EXECUTING, + PYBA_STATE_EXECUTED, + PYBA_STATE_APPLIED, + PYBA_STATE_FAILED, +}; + +static const int WM_PYBA_SHOW_HELP = WM_APP + 100; +static const int WM_PYBA_DETECT_PACKAGES = WM_APP + 101; +static const int WM_PYBA_PLAN_PACKAGES = WM_APP + 102; +static const int WM_PYBA_APPLY_PACKAGES = WM_APP + 103; +static const int WM_PYBA_CHANGE_STATE = WM_APP + 104; +static const int WM_PYBA_SHOW_FAILURE = WM_APP + 105; + +// This enum must be kept in the same order as the PAGE_NAMES array. +enum PAGE { + PAGE_LOADING, + PAGE_HELP, + PAGE_INSTALL, + PAGE_UPGRADE, + PAGE_SIMPLE_INSTALL, + PAGE_CUSTOM1, + PAGE_CUSTOM2, + PAGE_MODIFY, + PAGE_PROGRESS, + PAGE_PROGRESS_PASSIVE, + PAGE_SUCCESS, + PAGE_FAILURE, + COUNT_PAGE, +}; + +// This array must be kept in the same order as the PAGE enum. +static LPCWSTR PAGE_NAMES[] = { + L"Loading", + L"Help", + L"Install", + L"Upgrade", + L"SimpleInstall", + L"Custom1", + L"Custom2", + L"Modify", + L"Progress", + L"ProgressPassive", + L"Success", + L"Failure", +}; + +enum CONTROL_ID { + // Non-paged controls + ID_CLOSE_BUTTON = THEME_FIRST_ASSIGN_CONTROL_ID, + ID_MINIMIZE_BUTTON, + + // Welcome page + ID_INSTALL_BUTTON, + ID_INSTALL_CUSTOM_BUTTON, + ID_INSTALL_SIMPLE_BUTTON, + ID_INSTALL_UPGRADE_BUTTON, + ID_INSTALL_UPGRADE_CUSTOM_BUTTON, + ID_INSTALL_CANCEL_BUTTON, + ID_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, + + // Customize Page + ID_TARGETDIR_EDITBOX, + ID_CUSTOM_ASSOCIATE_FILES_CHECKBOX, + ID_CUSTOM_INSTALL_ALL_USERS_CHECKBOX, + ID_CUSTOM_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, + ID_CUSTOM_COMPILE_ALL_CHECKBOX, + ID_CUSTOM_BROWSE_BUTTON, + ID_CUSTOM_BROWSE_BUTTON_LABEL, + ID_CUSTOM_INSTALL_BUTTON, + ID_CUSTOM_NEXT_BUTTON, + ID_CUSTOM1_BACK_BUTTON, + ID_CUSTOM2_BACK_BUTTON, + ID_CUSTOM1_CANCEL_BUTTON, + ID_CUSTOM2_CANCEL_BUTTON, + + // Modify page + ID_MODIFY_BUTTON, + ID_REPAIR_BUTTON, + ID_UNINSTALL_BUTTON, + ID_MODIFY_CANCEL_BUTTON, + + // Progress page + ID_CACHE_PROGRESS_PACKAGE_TEXT, + ID_CACHE_PROGRESS_BAR, + ID_CACHE_PROGRESS_TEXT, + + ID_EXECUTE_PROGRESS_PACKAGE_TEXT, + ID_EXECUTE_PROGRESS_BAR, + ID_EXECUTE_PROGRESS_TEXT, + ID_EXECUTE_PROGRESS_ACTIONDATA_TEXT, + + ID_OVERALL_PROGRESS_PACKAGE_TEXT, + ID_OVERALL_PROGRESS_BAR, + ID_OVERALL_CALCULATED_PROGRESS_BAR, + ID_OVERALL_PROGRESS_TEXT, + + ID_PROGRESS_CANCEL_BUTTON, + + // Success page + ID_LAUNCH_BUTTON, + ID_SUCCESS_TEXT, + ID_SUCCESS_RESTART_TEXT, + ID_SUCCESS_RESTART_BUTTON, + ID_SUCCESS_CANCEL_BUTTON, + + // Failure page + ID_FAILURE_LOGFILE_LINK, + ID_FAILURE_MESSAGE_TEXT, + ID_FAILURE_RESTART_TEXT, + ID_FAILURE_RESTART_BUTTON, + ID_FAILURE_CANCEL_BUTTON +}; + +static THEME_ASSIGN_CONTROL_ID CONTROL_ID_NAMES[] = { + { ID_CLOSE_BUTTON, L"CloseButton" }, + { ID_MINIMIZE_BUTTON, L"MinimizeButton" }, + + { ID_INSTALL_BUTTON, L"InstallButton" }, + { ID_INSTALL_CUSTOM_BUTTON, L"InstallCustomButton" }, + { ID_INSTALL_SIMPLE_BUTTON, L"InstallSimpleButton" }, + { ID_INSTALL_UPGRADE_BUTTON, L"InstallUpgradeButton" }, + { ID_INSTALL_UPGRADE_CUSTOM_BUTTON, L"InstallUpgradeCustomButton" }, + { ID_INSTALL_CANCEL_BUTTON, L"InstallCancelButton" }, + { ID_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, L"InstallLauncherAllUsers" }, + + { ID_TARGETDIR_EDITBOX, L"TargetDir" }, + { ID_CUSTOM_ASSOCIATE_FILES_CHECKBOX, L"AssociateFiles" }, + { ID_CUSTOM_INSTALL_ALL_USERS_CHECKBOX, L"InstallAllUsers" }, + { ID_CUSTOM_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, L"CustomInstallLauncherAllUsers" }, + { ID_CUSTOM_COMPILE_ALL_CHECKBOX, L"CompileAll" }, + { ID_CUSTOM_BROWSE_BUTTON, L"CustomBrowseButton" }, + { ID_CUSTOM_BROWSE_BUTTON_LABEL, L"CustomBrowseButtonLabel" }, + { ID_CUSTOM_INSTALL_BUTTON, L"CustomInstallButton" }, + { ID_CUSTOM_NEXT_BUTTON, L"CustomNextButton" }, + { ID_CUSTOM1_BACK_BUTTON, L"Custom1BackButton" }, + { ID_CUSTOM2_BACK_BUTTON, L"Custom2BackButton" }, + { ID_CUSTOM1_CANCEL_BUTTON, L"Custom1CancelButton" }, + { ID_CUSTOM2_CANCEL_BUTTON, L"Custom2CancelButton" }, + + { ID_MODIFY_BUTTON, L"ModifyButton" }, + { ID_REPAIR_BUTTON, L"RepairButton" }, + { ID_UNINSTALL_BUTTON, L"UninstallButton" }, + { ID_MODIFY_CANCEL_BUTTON, L"ModifyCancelButton" }, + + { ID_CACHE_PROGRESS_PACKAGE_TEXT, L"CacheProgressPackageText" }, + { ID_CACHE_PROGRESS_BAR, L"CacheProgressbar" }, + { ID_CACHE_PROGRESS_TEXT, L"CacheProgressText" }, + { ID_EXECUTE_PROGRESS_PACKAGE_TEXT, L"ExecuteProgressPackageText" }, + { ID_EXECUTE_PROGRESS_BAR, L"ExecuteProgressbar" }, + { ID_EXECUTE_PROGRESS_TEXT, L"ExecuteProgressText" }, + { ID_EXECUTE_PROGRESS_ACTIONDATA_TEXT, L"ExecuteProgressActionDataText" }, + { ID_OVERALL_PROGRESS_PACKAGE_TEXT, L"OverallProgressPackageText" }, + { ID_OVERALL_PROGRESS_BAR, L"OverallProgressbar" }, + { ID_OVERALL_CALCULATED_PROGRESS_BAR, L"OverallCalculatedProgressbar" }, + { ID_OVERALL_PROGRESS_TEXT, L"OverallProgressText" }, + { ID_PROGRESS_CANCEL_BUTTON, L"ProgressCancelButton" }, + + { ID_LAUNCH_BUTTON, L"LaunchButton" }, + { ID_SUCCESS_TEXT, L"SuccessText" }, + { ID_SUCCESS_RESTART_TEXT, L"SuccessRestartText" }, + { ID_SUCCESS_RESTART_BUTTON, L"SuccessRestartButton" }, + { ID_SUCCESS_CANCEL_BUTTON, L"SuccessCancelButton" }, + + { ID_FAILURE_LOGFILE_LINK, L"FailureLogFileLink" }, + { ID_FAILURE_MESSAGE_TEXT, L"FailureMessageText" }, + { ID_FAILURE_RESTART_TEXT, L"FailureRestartText" }, + { ID_FAILURE_RESTART_BUTTON, L"FailureRestartButton" }, + { ID_FAILURE_CANCEL_BUTTON, L"FailureCancelButton" }, +}; + +static struct { LPCWSTR regName; LPCWSTR variableName; } OPTIONAL_FEATURES[] = { + { L"core_d", L"Include_debug" }, + { L"core_pdb", L"Include_symbols" }, + { L"dev", L"Include_dev" }, + { L"doc", L"Include_doc" }, + { L"exe", L"Include_exe" }, + { L"lib", L"Include_lib" }, + { L"path", L"PrependPath" }, + { L"pip", L"Include_pip" }, + { L"tcltk", L"Include_tcltk" }, + { L"test", L"Include_test" }, + { L"tools", L"Include_tools" }, + { L"Shortcuts", L"Shortcuts" }, + // Include_launcher and AssociateFiles are handled separately and so do + // not need to be included in this list. + { nullptr, nullptr } +}; + + + +class PythonBootstrapperApplication : public CBalBaseBootstrapperApplication { + void ShowPage(DWORD newPageId) { + // Process each control for special handling in the new page. + ProcessPageControls(ThemeGetPage(_theme, newPageId)); + + // Enable disable controls per-page. + if (_pageIds[PAGE_INSTALL] == newPageId || + _pageIds[PAGE_SIMPLE_INSTALL] == newPageId || + _pageIds[PAGE_UPGRADE] == newPageId) { + InstallPage_Show(); + } else if (_pageIds[PAGE_CUSTOM1] == newPageId) { + Custom1Page_Show(); + } else if (_pageIds[PAGE_CUSTOM2] == newPageId) { + Custom2Page_Show(); + } else if (_pageIds[PAGE_MODIFY] == newPageId) { + ModifyPage_Show(); + } else if (_pageIds[PAGE_SUCCESS] == newPageId) { + SuccessPage_Show(); + } else if (_pageIds[PAGE_FAILURE] == newPageId) { + FailurePage_Show(); + } + + // Prevent repainting while switching page to avoid ugly flickering + _suppressPaint = TRUE; + ThemeShowPage(_theme, newPageId, SW_SHOW); + ThemeShowPage(_theme, _visiblePageId, SW_HIDE); + _suppressPaint = FALSE; + InvalidateRect(_theme->hwndParent, nullptr, TRUE); + _visiblePageId = newPageId; + + // On the install page set the focus to the install button or + // the next enabled control if install is disabled + if (_pageIds[PAGE_INSTALL] == newPageId) { + ThemeSetFocus(_theme, ID_INSTALL_BUTTON); + } else if (_pageIds[PAGE_SIMPLE_INSTALL] == newPageId) { + ThemeSetFocus(_theme, ID_INSTALL_SIMPLE_BUTTON); + } + } + + // + // Handles control clicks + // + void OnCommand(CONTROL_ID id) { + LPWSTR defaultDir = nullptr; + LPWSTR targetDir = nullptr; + LONGLONG elevated, crtInstalled, installAllUsers; + BOOL checked, launcherChecked; + WCHAR wzPath[MAX_PATH] = { }; + BROWSEINFOW browseInfo = { }; + PIDLIST_ABSOLUTE pidl = nullptr; + DWORD pageId; + HRESULT hr = S_OK; + + switch(id) { + case ID_CLOSE_BUTTON: + OnClickCloseButton(); + break; + + // Install commands + case ID_INSTALL_SIMPLE_BUTTON: __fallthrough; + case ID_INSTALL_UPGRADE_BUTTON: __fallthrough; + case ID_INSTALL_BUTTON: + SavePageSettings(); + + if (!WillElevate() && !QueryElevateForCrtInstall()) { + break; + } + + hr = BalGetNumericVariable(L"InstallAllUsers", &installAllUsers); + ExitOnFailure(hr, L"Failed to get install scope"); + + hr = _engine->SetVariableNumeric(L"CompileAll", installAllUsers); + ExitOnFailure(hr, L"Failed to update CompileAll"); + + hr = BalGetStringVariable(L"TargetDir", &targetDir); + if (FAILED(hr) || !targetDir || !targetDir[0]) { + ReleaseStr(targetDir); + targetDir = nullptr; + + hr = BalGetStringVariable( + installAllUsers ? L"DefaultAllUsersTargetDir" : L"DefaultJustForMeTargetDir", + &defaultDir + ); + BalExitOnFailure(hr, "Failed to get the default install directory"); + + if (!defaultDir || !defaultDir[0]) { + BalLogError(E_INVALIDARG, "Default install directory is blank"); + } + + hr = BalFormatString(defaultDir, &targetDir); + BalExitOnFailure1(hr, "Failed to format '%ls'", defaultDir); + + hr = _engine->SetVariableString(L"TargetDir", targetDir); + BalExitOnFailure(hr, "Failed to set install target directory"); + } + ReleaseStr(targetDir); + + OnPlan(BOOTSTRAPPER_ACTION_INSTALL); + break; + + case ID_CUSTOM1_BACK_BUTTON: + SavePageSettings(); + if (_modifying) { + GoToPage(PAGE_MODIFY); + } else { + GoToPage(PAGE_INSTALL); + } + break; + + case ID_INSTALL_CUSTOM_BUTTON: __fallthrough; + case ID_INSTALL_UPGRADE_CUSTOM_BUTTON: __fallthrough; + case ID_CUSTOM2_BACK_BUTTON: + SavePageSettings(); + GoToPage(PAGE_CUSTOM1); + break; + + case ID_CUSTOM_NEXT_BUTTON: + SavePageSettings(); + GoToPage(PAGE_CUSTOM2); + break; + + case ID_CUSTOM_INSTALL_BUTTON: + SavePageSettings(); + + hr = BalGetStringVariable(L"TargetDir", &targetDir); + if (SUCCEEDED(hr)) { + // TODO: Check whether directory exists and contains another installation + ReleaseStr(targetDir); + } + + if (!WillElevate() && !QueryElevateForCrtInstall()) { + break; + } + + OnPlan(_command.action); + break; + + case ID_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX: + checked = ThemeIsControlChecked(_theme, ID_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX); + _engine->SetVariableNumeric(L"InstallLauncherAllUsers", checked); + + ThemeControlElevates(_theme, ID_INSTALL_BUTTON, WillElevate()); + break; + + case ID_CUSTOM_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX: + checked = ThemeIsControlChecked(_theme, ID_CUSTOM_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX); + _engine->SetVariableNumeric(L"InstallLauncherAllUsers", checked); + + ThemeControlElevates(_theme, ID_CUSTOM_INSTALL_BUTTON, WillElevate()); + break; + + case ID_CUSTOM_INSTALL_ALL_USERS_CHECKBOX: + checked = ThemeIsControlChecked(_theme, ID_CUSTOM_INSTALL_ALL_USERS_CHECKBOX); + _engine->SetVariableNumeric(L"InstallAllUsers", checked); + + ThemeControlElevates(_theme, ID_CUSTOM_INSTALL_BUTTON, WillElevate()); + ThemeControlEnable(_theme, ID_CUSTOM_BROWSE_BUTTON_LABEL, !checked); + if (checked) { + _engine->SetVariableNumeric(L"CompileAll", 1); + ThemeSendControlMessage(_theme, ID_CUSTOM_COMPILE_ALL_CHECKBOX, BM_SETCHECK, BST_CHECKED, 0); + } + ThemeGetTextControl(_theme, ID_TARGETDIR_EDITBOX, &targetDir); + if (targetDir) { + // Check the current value against the default to see + // if we should switch it automatically. + hr = BalGetStringVariable( + checked ? L"DefaultJustForMeTargetDir" : L"DefaultAllUsersTargetDir", + &defaultDir + ); + + if (SUCCEEDED(hr) && defaultDir) { + LPWSTR formatted = nullptr; + if (defaultDir[0] && SUCCEEDED(BalFormatString(defaultDir, &formatted))) { + if (wcscmp(formatted, targetDir) == 0) { + ReleaseStr(defaultDir); + defaultDir = nullptr; + ReleaseStr(formatted); + formatted = nullptr; + + hr = BalGetStringVariable( + checked ? L"DefaultAllUsersTargetDir" : L"DefaultJustForMeTargetDir", + &defaultDir + ); + if (SUCCEEDED(hr) && defaultDir && defaultDir[0] && SUCCEEDED(BalFormatString(defaultDir, &formatted))) { + ThemeSetTextControl(_theme, ID_TARGETDIR_EDITBOX, formatted); + ReleaseStr(formatted); + } + } else { + ReleaseStr(formatted); + } + } + + ReleaseStr(defaultDir); + } + } + break; + + case ID_CUSTOM_BROWSE_BUTTON: + browseInfo.hwndOwner = _hWnd; + browseInfo.pszDisplayName = wzPath; + browseInfo.lpszTitle = _theme->sczCaption; + browseInfo.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI; + pidl = ::SHBrowseForFolderW(&browseInfo); + if (pidl && ::SHGetPathFromIDListW(pidl, wzPath)) { + ThemeSetTextControl(_theme, ID_TARGETDIR_EDITBOX, wzPath); + } + + if (pidl) { + ::CoTaskMemFree(pidl); + } + break; + + // Modify commands + case ID_MODIFY_BUTTON: + // Some variables cannot be modified + _engine->SetVariableString(L"InstallAllUsersState", L"disable"); + _engine->SetVariableString(L"InstallLauncherAllUsersState", L"disable"); + _engine->SetVariableString(L"TargetDirState", L"disable"); + _engine->SetVariableString(L"CustomBrowseButtonState", L"disable"); + _modifying = TRUE; + GoToPage(PAGE_CUSTOM1); + break; + + case ID_REPAIR_BUTTON: + OnPlan(BOOTSTRAPPER_ACTION_REPAIR); + break; + + case ID_UNINSTALL_BUTTON: + OnPlan(BOOTSTRAPPER_ACTION_UNINSTALL); + break; + } + + LExit: + return; + } + + void InstallPage_Show() { + // Ensure the All Users install button has a UAC shield + BOOL elevated = WillElevate(); + ThemeControlElevates(_theme, ID_INSTALL_BUTTON, elevated); + ThemeControlElevates(_theme, ID_INSTALL_SIMPLE_BUTTON, elevated); + ThemeControlElevates(_theme, ID_INSTALL_UPGRADE_BUTTON, elevated); + } + + void Custom1Page_Show() { + LONGLONG installLauncherAllUsers; + + if (FAILED(BalGetNumericVariable(L"InstallLauncherAllUsers", &installLauncherAllUsers))) { + installLauncherAllUsers = 0; + } + + ThemeSendControlMessage(_theme, ID_CUSTOM_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, BM_SETCHECK, + installLauncherAllUsers ? BST_CHECKED : BST_UNCHECKED, 0); + } + + void Custom2Page_Show() { + HRESULT hr; + LONGLONG installAll, includeLauncher; + + if (FAILED(BalGetNumericVariable(L"InstallAllUsers", &installAll))) { + installAll = 0; + } + + if (WillElevate()) { + ThemeControlElevates(_theme, ID_CUSTOM_INSTALL_BUTTON, TRUE); + ThemeShowControl(_theme, ID_CUSTOM_BROWSE_BUTTON_LABEL, SW_HIDE); + } else { + ThemeControlElevates(_theme, ID_CUSTOM_INSTALL_BUTTON, FALSE); + ThemeShowControl(_theme, ID_CUSTOM_BROWSE_BUTTON_LABEL, SW_SHOW); + } + + if (SUCCEEDED(BalGetNumericVariable(L"Include_launcher", &includeLauncher)) && includeLauncher) { + ThemeControlEnable(_theme, ID_CUSTOM_ASSOCIATE_FILES_CHECKBOX, TRUE); + } else { + ThemeSendControlMessage(_theme, ID_CUSTOM_ASSOCIATE_FILES_CHECKBOX, BM_SETCHECK, BST_UNCHECKED, 0); + ThemeControlEnable(_theme, ID_CUSTOM_ASSOCIATE_FILES_CHECKBOX, FALSE); + } + + LPWSTR targetDir = nullptr; + hr = BalGetStringVariable(L"TargetDir", &targetDir); + if (SUCCEEDED(hr) && targetDir && targetDir[0]) { + ThemeSetTextControl(_theme, ID_TARGETDIR_EDITBOX, targetDir); + StrFree(targetDir); + } else if (SUCCEEDED(hr)) { + StrFree(targetDir); + targetDir = nullptr; + + LPWSTR defaultTargetDir = nullptr; + hr = BalGetStringVariable(L"DefaultCustomTargetDir", &defaultTargetDir); + if (SUCCEEDED(hr) && defaultTargetDir && !defaultTargetDir[0]) { + StrFree(defaultTargetDir); + defaultTargetDir = nullptr; + + hr = BalGetStringVariable( + installAll ? L"DefaultAllUsersTargetDir" : L"DefaultJustForMeTargetDir", + &defaultTargetDir + ); + } + if (SUCCEEDED(hr) && defaultTargetDir) { + if (defaultTargetDir[0] && SUCCEEDED(BalFormatString(defaultTargetDir, &targetDir))) { + ThemeSetTextControl(_theme, ID_TARGETDIR_EDITBOX, targetDir); + StrFree(targetDir); + } + StrFree(defaultTargetDir); + } + } + } + + void ModifyPage_Show() { + ThemeControlEnable(_theme, ID_REPAIR_BUTTON, !_suppressRepair); + } + + void SuccessPage_Show() { + // on the "Success" page, check if the restart or launch button should be enabled. + BOOL showRestartButton = FALSE; + BOOL launchTargetExists = FALSE; + LOC_STRING *successText = nullptr; + HRESULT hr = S_OK; + + if (_restartRequired) { + if (BOOTSTRAPPER_RESTART_PROMPT == _command.restart) { + showRestartButton = TRUE; + } + } else if (ThemeControlExists(_theme, ID_LAUNCH_BUTTON)) { + launchTargetExists = BalStringVariableExists(PYBA_VARIABLE_LAUNCH_TARGET_PATH); + } + + switch (_plannedAction) { + case BOOTSTRAPPER_ACTION_INSTALL: + hr = LocGetString(_wixLoc, L"#(loc.SuccessInstallMessage)", &successText); + break; + case BOOTSTRAPPER_ACTION_MODIFY: + hr = LocGetString(_wixLoc, L"#(loc.SuccessModifyMessage)", &successText); + break; + case BOOTSTRAPPER_ACTION_REPAIR: + hr = LocGetString(_wixLoc, L"#(loc.SuccessRepairMessage)", &successText); + break; + case BOOTSTRAPPER_ACTION_UNINSTALL: + hr = LocGetString(_wixLoc, L"#(loc.SuccessRemoveMessage)", &successText); + break; + } + + if (successText) { + LPWSTR formattedString = nullptr; + BalFormatString(successText->wzText, &formattedString); + if (formattedString) { + ThemeSetTextControl(_theme, ID_SUCCESS_TEXT, formattedString); + StrFree(formattedString); + } + } + + ThemeControlEnable(_theme, ID_LAUNCH_BUTTON, launchTargetExists && BOOTSTRAPPER_ACTION_UNINSTALL < _plannedAction); + ThemeControlEnable(_theme, ID_SUCCESS_RESTART_TEXT, showRestartButton); + ThemeControlEnable(_theme, ID_SUCCESS_RESTART_BUTTON, showRestartButton); + } + + void FailurePage_Show() { + // on the "Failure" page, show error message and check if the restart button should be enabled. + + // if there is a log file variable then we'll assume the log file exists. + BOOL showLogLink = (_bundle.sczLogVariable && *_bundle.sczLogVariable); + BOOL showErrorMessage = FALSE; + BOOL showRestartButton = FALSE; + + if (FAILED(_hrFinal)) { + LPWSTR unformattedText = nullptr; + LPWSTR text = nullptr; + + // If we know the failure message, use that. + if (_failedMessage && *_failedMessage) { + StrAllocString(&unformattedText, _failedMessage, 0); + } else { + // try to get the error message from the error code. + StrAllocFromError(&unformattedText, _hrFinal, nullptr); + if (!unformattedText || !*unformattedText) { + StrAllocFromError(&unformattedText, E_FAIL, nullptr); + } + } + + if (E_WIXSTDBA_CONDITION_FAILED == _hrFinal) { + if (unformattedText) { + StrAllocString(&text, unformattedText, 0); + } + } else { + StrAllocFormatted(&text, L"0x%08x - %ls", _hrFinal, unformattedText); + } + + if (text) { + ThemeSetTextControl(_theme, ID_FAILURE_MESSAGE_TEXT, text); + showErrorMessage = TRUE; + } + + ReleaseStr(text); + ReleaseStr(unformattedText); + } + + if (_restartRequired && BOOTSTRAPPER_RESTART_PROMPT == _command.restart) { + showRestartButton = TRUE; + } + + ThemeControlEnable(_theme, ID_FAILURE_LOGFILE_LINK, showLogLink); + ThemeControlEnable(_theme, ID_FAILURE_MESSAGE_TEXT, showErrorMessage); + ThemeControlEnable(_theme, ID_FAILURE_RESTART_TEXT, showRestartButton); + ThemeControlEnable(_theme, ID_FAILURE_RESTART_BUTTON, showRestartButton); + } + + +public: // IBootstrapperApplication + virtual STDMETHODIMP OnStartup() { + HRESULT hr = S_OK; + DWORD dwUIThreadId = 0; + + // create UI thread + _hUiThread = ::CreateThread(nullptr, 0, UiThreadProc, this, 0, &dwUIThreadId); + if (!_hUiThread) { + ExitWithLastError(hr, "Failed to create UI thread."); + } + + LExit: + return hr; + } + + + virtual STDMETHODIMP_(int) OnShutdown() { + int nResult = IDNOACTION; + + // wait for UI thread to terminate + if (_hUiThread) { + ::WaitForSingleObject(_hUiThread, INFINITE); + ReleaseHandle(_hUiThread); + } + + // If a restart was required. + if (_restartRequired && _allowRestart) { + nResult = IDRESTART; + } + + return nResult; + } + + + virtual STDMETHODIMP_(int) OnDetectRelatedBundle( + __in LPCWSTR wzBundleId, + __in BOOTSTRAPPER_RELATION_TYPE relationType, + __in LPCWSTR /*wzBundleTag*/, + __in BOOL fPerMachine, + __in DWORD64 /*dw64Version*/, + __in BOOTSTRAPPER_RELATED_OPERATION operation + ) { + BalInfoAddRelatedBundleAsPackage(&_bundle.packages, wzBundleId, relationType, fPerMachine); + + // Remember when our bundle would cause a downgrade. + if (BOOTSTRAPPER_RELATED_OPERATION_DOWNGRADE == operation) { + _downgradingOtherVersion = TRUE; + } else if (BOOTSTRAPPER_RELATED_OPERATION_MAJOR_UPGRADE == operation) { + _upgradingOldVersion = TRUE; + + // Assume we don't want the launcher or file associations, and if + // they have already been installed then loading the state will + // reactivate these settings. + _engine->SetVariableNumeric(L"Include_launcher", 0); + _engine->SetVariableNumeric(L"AssociateFiles", 0); + auto hr = LoadLauncherStateFromKey(_engine, HKEY_CURRENT_USER); + if (hr == S_FALSE) { + hr = LoadLauncherStateFromKey(_engine, HKEY_LOCAL_MACHINE); + } + } else if (BOOTSTRAPPER_RELATED_OPERATION_NONE == operation) { + if (_command.action == BOOTSTRAPPER_ACTION_INSTALL) { + LOC_STRING *pLocString = nullptr; + if (SUCCEEDED(LocGetString(_wixLoc, L"#(loc.FailureExistingInstall)", &pLocString)) && pLocString) { + BalFormatString(pLocString->wzText, &_failedMessage); + } else { + BalFormatString(L"Cannot install [WixBundleName] because it is already installed.", &_failedMessage); + } + BalLog( + BOOTSTRAPPER_LOG_LEVEL_ERROR, + "Related bundle %ls is preventing install", + wzBundleId + ); + SetState(PYBA_STATE_FAILED, E_WIXSTDBA_CONDITION_FAILED); + } + } + + return CheckCanceled() ? IDCANCEL : IDOK; + } + + + virtual STDMETHODIMP_(void) OnDetectPackageComplete( + __in LPCWSTR wzPackageId, + __in HRESULT /*hrStatus*/, + __in BOOTSTRAPPER_PACKAGE_STATE state + ) { } + + + virtual STDMETHODIMP_(void) OnDetectComplete(__in HRESULT hrStatus) { + if (SUCCEEDED(hrStatus) && _baFunction) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Running detect complete BA function"); + _baFunction->OnDetectComplete(); + } + + if (SUCCEEDED(hrStatus)) { + hrStatus = EvaluateConditions(); + } + + SetState(PYBA_STATE_DETECTED, hrStatus); + + // If we're not interacting with the user or we're doing a layout or we're just after a force restart + // then automatically start planning. + if (BOOTSTRAPPER_DISPLAY_FULL > _command.display || + BOOTSTRAPPER_ACTION_LAYOUT == _command.action || + BOOTSTRAPPER_ACTION_UNINSTALL == _command.action || + BOOTSTRAPPER_RESUME_TYPE_REBOOT == _command.resumeType) { + if (SUCCEEDED(hrStatus)) { + ::PostMessageW(_hWnd, WM_PYBA_PLAN_PACKAGES, 0, _command.action); + } + } + } + + + virtual STDMETHODIMP_(int) OnPlanRelatedBundle( + __in_z LPCWSTR /*wzBundleId*/, + __inout_z BOOTSTRAPPER_REQUEST_STATE* pRequestedState + ) { + return CheckCanceled() ? IDCANCEL : IDOK; + } + + + virtual STDMETHODIMP_(int) OnPlanPackageBegin( + __in_z LPCWSTR wzPackageId, + __inout BOOTSTRAPPER_REQUEST_STATE *pRequestState + ) { + HRESULT hr = S_OK; + BAL_INFO_PACKAGE* pPackage = nullptr; + + if (_nextPackageAfterRestart) { + // After restart we need to finish the dependency registration for our package so allow the package + // to go present. + if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, _nextPackageAfterRestart, -1)) { + // Do not allow a repair because that could put us in a perpetual restart loop. + if (BOOTSTRAPPER_REQUEST_STATE_REPAIR == *pRequestState) { + *pRequestState = BOOTSTRAPPER_REQUEST_STATE_PRESENT; + } + + ReleaseNullStr(_nextPackageAfterRestart); // no more skipping now. + } else { + // not the matching package, so skip it. + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Skipping package: %ls, after restart because it was applied before the restart.", wzPackageId); + + *pRequestState = BOOTSTRAPPER_REQUEST_STATE_NONE; + } + } else if ((_plannedAction == BOOTSTRAPPER_ACTION_INSTALL || _plannedAction == BOOTSTRAPPER_ACTION_MODIFY) && + SUCCEEDED(BalInfoFindPackageById(&_bundle.packages, wzPackageId, &pPackage))) { + BOOL f = FALSE; + if (SUCCEEDED(_engine->EvaluateCondition(pPackage->sczInstallCondition, &f)) && f) { + *pRequestState = BOOTSTRAPPER_REQUEST_STATE_PRESENT; + } + } + + return CheckCanceled() ? IDCANCEL : IDOK; + } + + virtual STDMETHODIMP_(int) OnPlanMsiFeature( + __in_z LPCWSTR wzPackageId, + __in_z LPCWSTR wzFeatureId, + __inout BOOTSTRAPPER_FEATURE_STATE* pRequestedState + ) { + LONGLONG install; + + if (wcscmp(wzFeatureId, L"AssociateFiles") == 0 || wcscmp(wzFeatureId, L"Shortcuts") == 0) { + if (SUCCEEDED(_engine->GetVariableNumeric(wzFeatureId, &install)) && install) { + *pRequestedState = BOOTSTRAPPER_FEATURE_STATE_LOCAL; + } else { + *pRequestedState = BOOTSTRAPPER_FEATURE_STATE_ABSENT; + } + } else { + *pRequestedState = BOOTSTRAPPER_FEATURE_STATE_LOCAL; + } + return CheckCanceled() ? IDCANCEL : IDNOACTION; + } + + virtual STDMETHODIMP_(void) OnPlanComplete(__in HRESULT hrStatus) { + if (SUCCEEDED(hrStatus) && _baFunction) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Running plan complete BA function"); + _baFunction->OnPlanComplete(); + } + + SetState(PYBA_STATE_PLANNED, hrStatus); + + if (SUCCEEDED(hrStatus)) { + ::PostMessageW(_hWnd, WM_PYBA_APPLY_PACKAGES, 0, 0); + } + + _startedExecution = FALSE; + _calculatedCacheProgress = 0; + _calculatedExecuteProgress = 0; + } + + + virtual STDMETHODIMP_(int) OnCachePackageBegin( + __in_z LPCWSTR wzPackageId, + __in DWORD cCachePayloads, + __in DWORD64 dw64PackageCacheSize + ) { + if (wzPackageId && *wzPackageId) { + BAL_INFO_PACKAGE* pPackage = nullptr; + HRESULT hr = BalInfoFindPackageById(&_bundle.packages, wzPackageId, &pPackage); + LPCWSTR wz = (SUCCEEDED(hr) && pPackage->sczDisplayName) ? pPackage->sczDisplayName : wzPackageId; + + ThemeSetTextControl(_theme, ID_CACHE_PROGRESS_PACKAGE_TEXT, wz); + + // If something started executing, leave it in the overall progress text. + if (!_startedExecution) { + ThemeSetTextControl(_theme, ID_OVERALL_PROGRESS_PACKAGE_TEXT, wz); + } + } + + return __super::OnCachePackageBegin(wzPackageId, cCachePayloads, dw64PackageCacheSize); + } + + + virtual STDMETHODIMP_(int) OnCacheAcquireProgress( + __in_z LPCWSTR wzPackageOrContainerId, + __in_z_opt LPCWSTR wzPayloadId, + __in DWORD64 dw64Progress, + __in DWORD64 dw64Total, + __in DWORD dwOverallPercentage + ) { + WCHAR wzProgress[5] = { }; + +#ifdef DEBUG + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "PYBA: OnCacheAcquireProgress() - container/package: %ls, payload: %ls, progress: %I64u, total: %I64u, overall progress: %u%%", wzPackageOrContainerId, wzPayloadId, dw64Progress, dw64Total, dwOverallPercentage); +#endif + + ::StringCchPrintfW(wzProgress, countof(wzProgress), L"%u%%", dwOverallPercentage); + ThemeSetTextControl(_theme, ID_CACHE_PROGRESS_TEXT, wzProgress); + + ThemeSetProgressControl(_theme, ID_CACHE_PROGRESS_BAR, dwOverallPercentage); + + _calculatedCacheProgress = dwOverallPercentage * PYBA_ACQUIRE_PERCENTAGE / 100; + ThemeSetProgressControl(_theme, ID_OVERALL_CALCULATED_PROGRESS_BAR, _calculatedCacheProgress + _calculatedExecuteProgress); + + SetTaskbarButtonProgress(_calculatedCacheProgress + _calculatedExecuteProgress); + + return __super::OnCacheAcquireProgress(wzPackageOrContainerId, wzPayloadId, dw64Progress, dw64Total, dwOverallPercentage); + } + + + virtual STDMETHODIMP_(int) OnCacheAcquireComplete( + __in_z LPCWSTR wzPackageOrContainerId, + __in_z_opt LPCWSTR wzPayloadId, + __in HRESULT hrStatus, + __in int nRecommendation + ) { + SetProgressState(hrStatus); + return __super::OnCacheAcquireComplete(wzPackageOrContainerId, wzPayloadId, hrStatus, nRecommendation); + } + + + virtual STDMETHODIMP_(int) OnCacheVerifyComplete( + __in_z LPCWSTR wzPackageId, + __in_z LPCWSTR wzPayloadId, + __in HRESULT hrStatus, + __in int nRecommendation + ) { + SetProgressState(hrStatus); + return __super::OnCacheVerifyComplete(wzPackageId, wzPayloadId, hrStatus, nRecommendation); + } + + + virtual STDMETHODIMP_(void) OnCacheComplete(__in HRESULT /*hrStatus*/) { + ThemeSetTextControl(_theme, ID_CACHE_PROGRESS_PACKAGE_TEXT, L""); + SetState(PYBA_STATE_CACHED, S_OK); // we always return success here and let OnApplyComplete() deal with the error. + } + + + virtual STDMETHODIMP_(int) OnError( + __in BOOTSTRAPPER_ERROR_TYPE errorType, + __in LPCWSTR wzPackageId, + __in DWORD dwCode, + __in_z LPCWSTR wzError, + __in DWORD dwUIHint, + __in DWORD /*cData*/, + __in_ecount_z_opt(cData) LPCWSTR* /*rgwzData*/, + __in int nRecommendation + ) { + int nResult = nRecommendation; + LPWSTR sczError = nullptr; + + if (BOOTSTRAPPER_DISPLAY_EMBEDDED == _command.display) { + HRESULT hr = _engine->SendEmbeddedError(dwCode, wzError, dwUIHint, &nResult); + if (FAILED(hr)) { + nResult = IDERROR; + } + } else if (BOOTSTRAPPER_DISPLAY_FULL == _command.display) { + // If this is an authentication failure, let the engine try to handle it for us. + if (BOOTSTRAPPER_ERROR_TYPE_HTTP_AUTH_SERVER == errorType || BOOTSTRAPPER_ERROR_TYPE_HTTP_AUTH_PROXY == errorType) { + nResult = IDTRYAGAIN; + } else // show a generic error message box. + { + BalRetryErrorOccurred(wzPackageId, dwCode); + + if (!_showingInternalUIThisPackage) { + // If no error message was provided, use the error code to try and get an error message. + if (!wzError || !*wzError || BOOTSTRAPPER_ERROR_TYPE_WINDOWS_INSTALLER != errorType) { + HRESULT hr = StrAllocFromError(&sczError, dwCode, nullptr); + if (FAILED(hr) || !sczError || !*sczError) { + StrAllocFormatted(&sczError, L"0x%x", dwCode); + } + } + + nResult = ::MessageBoxW(_hWnd, sczError ? sczError : wzError, _theme->sczCaption, dwUIHint); + } + } + + SetProgressState(HRESULT_FROM_WIN32(dwCode)); + } else { + // just take note of the error code and let things continue. + BalRetryErrorOccurred(wzPackageId, dwCode); + } + + ReleaseStr(sczError); + return nResult; + } + + + virtual STDMETHODIMP_(int) OnExecuteMsiMessage( + __in_z LPCWSTR wzPackageId, + __in INSTALLMESSAGE mt, + __in UINT uiFlags, + __in_z LPCWSTR wzMessage, + __in DWORD cData, + __in_ecount_z_opt(cData) LPCWSTR* rgwzData, + __in int nRecommendation + ) { +#ifdef DEBUG + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "PYBA: OnExecuteMsiMessage() - package: %ls, message: %ls", wzPackageId, wzMessage); +#endif + if (BOOTSTRAPPER_DISPLAY_FULL == _command.display && (INSTALLMESSAGE_WARNING == mt || INSTALLMESSAGE_USER == mt)) { + int nResult = ::MessageBoxW(_hWnd, wzMessage, _theme->sczCaption, uiFlags); + return nResult; + } + + if (INSTALLMESSAGE_ACTIONSTART == mt) { + ThemeSetTextControl(_theme, ID_EXECUTE_PROGRESS_ACTIONDATA_TEXT, wzMessage); + } + + return __super::OnExecuteMsiMessage(wzPackageId, mt, uiFlags, wzMessage, cData, rgwzData, nRecommendation); + } + + + virtual STDMETHODIMP_(int) OnProgress(__in DWORD dwProgressPercentage, __in DWORD dwOverallProgressPercentage) { + WCHAR wzProgress[5] = { }; + +#ifdef DEBUG + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "PYBA: OnProgress() - progress: %u%%, overall progress: %u%%", dwProgressPercentage, dwOverallProgressPercentage); +#endif + + ::StringCchPrintfW(wzProgress, countof(wzProgress), L"%u%%", dwOverallProgressPercentage); + ThemeSetTextControl(_theme, ID_OVERALL_PROGRESS_TEXT, wzProgress); + + ThemeSetProgressControl(_theme, ID_OVERALL_PROGRESS_BAR, dwOverallProgressPercentage); + SetTaskbarButtonProgress(dwOverallProgressPercentage); + + return __super::OnProgress(dwProgressPercentage, dwOverallProgressPercentage); + } + + + virtual STDMETHODIMP_(int) OnExecutePackageBegin(__in_z LPCWSTR wzPackageId, __in BOOL fExecute) { + LPWSTR sczFormattedString = nullptr; + + _startedExecution = TRUE; + + if (wzPackageId && *wzPackageId) { + BAL_INFO_PACKAGE* pPackage = nullptr; + BalInfoFindPackageById(&_bundle.packages, wzPackageId, &pPackage); + + LPCWSTR wz = wzPackageId; + if (pPackage) { + LOC_STRING* pLocString = nullptr; + + switch (pPackage->type) { + case BAL_INFO_PACKAGE_TYPE_BUNDLE_ADDON: + LocGetString(_wixLoc, L"#(loc.ExecuteAddonRelatedBundleMessage)", &pLocString); + break; + + case BAL_INFO_PACKAGE_TYPE_BUNDLE_PATCH: + LocGetString(_wixLoc, L"#(loc.ExecutePatchRelatedBundleMessage)", &pLocString); + break; + + case BAL_INFO_PACKAGE_TYPE_BUNDLE_UPGRADE: + LocGetString(_wixLoc, L"#(loc.ExecuteUpgradeRelatedBundleMessage)", &pLocString); + break; + } + + if (pLocString) { + // If the wix developer is showing a hidden variable in the UI, then obviously they don't care about keeping it safe + // so don't go down the rabbit hole of making sure that this is securely freed. + BalFormatString(pLocString->wzText, &sczFormattedString); + } + + wz = sczFormattedString ? sczFormattedString : pPackage->sczDisplayName ? pPackage->sczDisplayName : wzPackageId; + } + + _showingInternalUIThisPackage = pPackage && pPackage->fDisplayInternalUI; + + ThemeSetTextControl(_theme, ID_EXECUTE_PROGRESS_PACKAGE_TEXT, wz); + ThemeSetTextControl(_theme, ID_OVERALL_PROGRESS_PACKAGE_TEXT, wz); + } else { + _showingInternalUIThisPackage = FALSE; + } + + ReleaseStr(sczFormattedString); + return __super::OnExecutePackageBegin(wzPackageId, fExecute); + } + + + virtual int __stdcall OnExecuteProgress( + __in_z LPCWSTR wzPackageId, + __in DWORD dwProgressPercentage, + __in DWORD dwOverallProgressPercentage + ) { + WCHAR wzProgress[8] = { }; + +#ifdef DEBUG + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "PYBA: OnExecuteProgress() - package: %ls, progress: %u%%, overall progress: %u%%", wzPackageId, dwProgressPercentage, dwOverallProgressPercentage); +#endif + + ::StringCchPrintfW(wzProgress, countof(wzProgress), L"%u%%", dwOverallProgressPercentage); + ThemeSetTextControl(_theme, ID_EXECUTE_PROGRESS_TEXT, wzProgress); + + ThemeSetProgressControl(_theme, ID_EXECUTE_PROGRESS_BAR, dwOverallProgressPercentage); + + _calculatedExecuteProgress = dwOverallProgressPercentage * (100 - PYBA_ACQUIRE_PERCENTAGE) / 100; + ThemeSetProgressControl(_theme, ID_OVERALL_CALCULATED_PROGRESS_BAR, _calculatedCacheProgress + _calculatedExecuteProgress); + + SetTaskbarButtonProgress(_calculatedCacheProgress + _calculatedExecuteProgress); + + return __super::OnExecuteProgress(wzPackageId, dwProgressPercentage, dwOverallProgressPercentage); + } + + + virtual STDMETHODIMP_(int) OnExecutePackageComplete( + __in_z LPCWSTR wzPackageId, + __in HRESULT hrExitCode, + __in BOOTSTRAPPER_APPLY_RESTART restart, + __in int nRecommendation + ) { + SetProgressState(hrExitCode); + + if (_wcsnicmp(wzPackageId, L"path_", 5) == 0 && SUCCEEDED(hrExitCode)) { + SendMessageTimeoutW( + HWND_BROADCAST, + WM_SETTINGCHANGE, + 0, + reinterpret_cast<LPARAM>(L"Environment"), + SMTO_ABORTIFHUNG, + 1000, + nullptr + ); + } + + int nResult = __super::OnExecutePackageComplete(wzPackageId, hrExitCode, restart, nRecommendation); + + return nResult; + } + + + virtual STDMETHODIMP_(void) OnExecuteComplete(__in HRESULT hrStatus) { + ThemeSetTextControl(_theme, ID_EXECUTE_PROGRESS_PACKAGE_TEXT, L""); + ThemeSetTextControl(_theme, ID_EXECUTE_PROGRESS_ACTIONDATA_TEXT, L""); + ThemeSetTextControl(_theme, ID_OVERALL_PROGRESS_PACKAGE_TEXT, L""); + ThemeControlEnable(_theme, ID_PROGRESS_CANCEL_BUTTON, FALSE); // no more cancel. + + SetState(PYBA_STATE_EXECUTED, S_OK); // we always return success here and let OnApplyComplete() deal with the error. + SetProgressState(hrStatus); + } + + + virtual STDMETHODIMP_(int) OnResolveSource( + __in_z LPCWSTR wzPackageOrContainerId, + __in_z_opt LPCWSTR wzPayloadId, + __in_z LPCWSTR wzLocalSource, + __in_z_opt LPCWSTR wzDownloadSource + ) { + int nResult = IDERROR; // assume we won't resolve source and that is unexpected. + + if (BOOTSTRAPPER_DISPLAY_FULL == _command.display) { + if (wzDownloadSource) { + nResult = IDDOWNLOAD; + } else { + // prompt to change the source location. + OPENFILENAMEW ofn = { }; + WCHAR wzFile[MAX_PATH] = { }; + + ::StringCchCopyW(wzFile, countof(wzFile), wzLocalSource); + + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _hWnd; + ofn.lpstrFile = wzFile; + ofn.nMaxFile = countof(wzFile); + ofn.lpstrFilter = L"All Files\0*.*\0"; + ofn.nFilterIndex = 1; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + ofn.lpstrTitle = _theme->sczCaption; + + if (::GetOpenFileNameW(&ofn)) { + HRESULT hr = _engine->SetLocalSource(wzPackageOrContainerId, wzPayloadId, ofn.lpstrFile); + nResult = SUCCEEDED(hr) ? IDRETRY : IDERROR; + } else { + nResult = IDCANCEL; + } + } + } else if (wzDownloadSource) { + // If doing a non-interactive install and download source is available, let's try downloading the package silently + nResult = IDDOWNLOAD; + } + // else there's nothing more we can do in non-interactive mode + + return CheckCanceled() ? IDCANCEL : nResult; + } + + + virtual STDMETHODIMP_(int) OnApplyComplete(__in HRESULT hrStatus, __in BOOTSTRAPPER_APPLY_RESTART restart) { + _restartResult = restart; // remember the restart result so we return the correct error code no matter what the user chooses to do in the UI. + + // If a restart was encountered and we are not suppressing restarts, then restart is required. + _restartRequired = (BOOTSTRAPPER_APPLY_RESTART_NONE != restart && BOOTSTRAPPER_RESTART_NEVER < _command.restart); + // If a restart is required and we're not displaying a UI or we are not supposed to prompt for restart then allow the restart. + _allowRestart = _restartRequired && (BOOTSTRAPPER_DISPLAY_FULL > _command.display || BOOTSTRAPPER_RESTART_PROMPT < _command.restart); + + // If we are showing UI, wait a beat before moving to the final screen. + if (BOOTSTRAPPER_DISPLAY_NONE < _command.display) { + ::Sleep(250); + } + + SetState(PYBA_STATE_APPLIED, hrStatus); + SetTaskbarButtonProgress(100); // show full progress bar, green, yellow, or red + + return IDNOACTION; + } + + virtual STDMETHODIMP_(void) OnLaunchApprovedExeComplete(__in HRESULT hrStatus, __in DWORD /*processId*/) { + if (HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) == hrStatus) { + //try with ShelExec next time + OnClickLaunchButton(); + } else { + ::PostMessageW(_hWnd, WM_CLOSE, 0, 0); + } + } + + +private: + // + // UiThreadProc - entrypoint for UI thread. + // + static DWORD WINAPI UiThreadProc(__in LPVOID pvContext) { + HRESULT hr = S_OK; + PythonBootstrapperApplication* pThis = (PythonBootstrapperApplication*)pvContext; + BOOL comInitialized = FALSE; + BOOL ret = FALSE; + MSG msg = { }; + + // Initialize COM and theme. + hr = ::CoInitialize(nullptr); + BalExitOnFailure(hr, "Failed to initialize COM."); + comInitialized = TRUE; + + hr = ThemeInitialize(pThis->_hModule); + BalExitOnFailure(hr, "Failed to initialize theme manager."); + + hr = pThis->InitializeData(); + BalExitOnFailure(hr, "Failed to initialize data in bootstrapper application."); + + // Create main window. + pThis->InitializeTaskbarButton(); + hr = pThis->CreateMainWindow(); + BalExitOnFailure(hr, "Failed to create main window."); + + if (FAILED(pThis->_hrFinal)) { + pThis->SetState(PYBA_STATE_FAILED, hr); + ::PostMessageW(pThis->_hWnd, WM_PYBA_SHOW_FAILURE, 0, 0); + } else { + // Okay, we're ready for packages now. + pThis->SetState(PYBA_STATE_INITIALIZED, hr); + ::PostMessageW(pThis->_hWnd, BOOTSTRAPPER_ACTION_HELP == pThis->_command.action ? WM_PYBA_SHOW_HELP : WM_PYBA_DETECT_PACKAGES, 0, 0); + } + + // message pump + while (0 != (ret = ::GetMessageW(&msg, nullptr, 0, 0))) { + if (-1 == ret) { + hr = E_UNEXPECTED; + BalExitOnFailure(hr, "Unexpected return value from message pump."); + } else if (!ThemeHandleKeyboardMessage(pThis->_theme, msg.hwnd, &msg)) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + } + + // Succeeded thus far, check to see if anything went wrong while actually + // executing changes. + if (FAILED(pThis->_hrFinal)) { + hr = pThis->_hrFinal; + } else if (pThis->CheckCanceled()) { + hr = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT); + } + + LExit: + // destroy main window + pThis->DestroyMainWindow(); + + // initiate engine shutdown + DWORD dwQuit = HRESULT_CODE(hr); + if (BOOTSTRAPPER_APPLY_RESTART_INITIATED == pThis->_restartResult) { + dwQuit = ERROR_SUCCESS_REBOOT_INITIATED; + } else if (BOOTSTRAPPER_APPLY_RESTART_REQUIRED == pThis->_restartResult) { + dwQuit = ERROR_SUCCESS_REBOOT_REQUIRED; + } + pThis->_engine->Quit(dwQuit); + + ReleaseTheme(pThis->_theme); + ThemeUninitialize(); + + // uninitialize COM + if (comInitialized) { + ::CoUninitialize(); + } + + return hr; + } + + // + // ParseVariablesFromUnattendXml - reads options from unattend.xml if it + // exists + // + HRESULT ParseVariablesFromUnattendXml() { + HRESULT hr = S_OK; + LPWSTR sczUnattendXmlPath = nullptr; + IXMLDOMDocument *pixdUnattend = nullptr; + IXMLDOMNodeList *pNodes = nullptr; + IXMLDOMNode *pNode = nullptr; + long cNodes; + DWORD dwAttr; + LPWSTR scz = nullptr; + BOOL bValue; + int iValue; + BOOL tryConvert; + BSTR bstrValue = nullptr; + + hr = BalFormatString(L"[WixBundleOriginalSourceFolder]unattend.xml", &sczUnattendXmlPath); + BalExitOnFailure(hr, "Failed to calculate path to unattend.xml"); + + if (!FileExistsEx(sczUnattendXmlPath, &dwAttr)) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_VERBOSE, "Did not find %ls", sczUnattendXmlPath); + hr = S_FALSE; + goto LExit; + } + + hr = XmlLoadDocumentFromFile(sczUnattendXmlPath, &pixdUnattend); + BalExitOnFailure1(hr, "Failed to read %ls", sczUnattendXmlPath); + + // get the list of variables users have overridden + hr = XmlSelectNodes(pixdUnattend, L"/Options/Option", &pNodes); + if (S_FALSE == hr) { + ExitFunction1(hr = S_OK); + } + BalExitOnFailure(hr, "Failed to select option nodes."); + + hr = pNodes->get_length((long*)&cNodes); + BalExitOnFailure(hr, "Failed to get option node count."); + + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Reading settings from %ls", sczUnattendXmlPath); + + for (DWORD i = 0; i < cNodes; ++i) { + hr = XmlNextElement(pNodes, &pNode, nullptr); + BalExitOnFailure(hr, "Failed to get next node."); + + // @Name + hr = XmlGetAttributeEx(pNode, L"Name", &scz); + BalExitOnFailure(hr, "Failed to get @Name."); + + tryConvert = TRUE; + hr = XmlGetAttribute(pNode, L"Value", &bstrValue); + if (FAILED(hr) || !bstrValue || !*bstrValue) { + hr = XmlGetText(pNode, &bstrValue); + tryConvert = FALSE; + } + BalExitOnFailure(hr, "Failed to get @Value."); + + if (tryConvert && + CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, bstrValue, -1, L"yes", -1)) { + _engine->SetVariableNumeric(scz, 1); + } else if (tryConvert && + CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, bstrValue, -1, L"no", -1)) { + _engine->SetVariableNumeric(scz, 0); + } else if (tryConvert && ::StrToIntExW(bstrValue, STIF_DEFAULT, &iValue)) { + _engine->SetVariableNumeric(scz, iValue); + } else { + _engine->SetVariableString(scz, bstrValue); + } + + ReleaseNullBSTR(bstrValue); + ReleaseNullStr(scz); + ReleaseNullObject(pNode); + } + + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Finished reading from %ls", sczUnattendXmlPath); + + LExit: + ReleaseObject(pNode); + ReleaseObject(pNodes); + ReleaseObject(pixdUnattend); + ReleaseStr(sczUnattendXmlPath); + + return hr; + } + + + // + // InitializeData - initializes all the package information. + // + HRESULT InitializeData() { + HRESULT hr = S_OK; + LPWSTR sczModulePath = nullptr; + IXMLDOMDocument *pixdManifest = nullptr; + + hr = BalManifestLoad(_hModule, &pixdManifest); + BalExitOnFailure(hr, "Failed to load bootstrapper application manifest."); + + hr = ParseOverridableVariablesFromXml(pixdManifest); + BalExitOnFailure(hr, "Failed to read overridable variables."); + + hr = ParseVariablesFromUnattendXml(); + ExitOnFailure(hr, "Failed to read unattend.ini file."); + + hr = ProcessCommandLine(&_language); + ExitOnFailure(hr, "Unknown commandline parameters."); + + hr = PathRelativeToModule(&sczModulePath, nullptr, _hModule); + BalExitOnFailure(hr, "Failed to get module path."); + + hr = LoadLocalization(sczModulePath, _language); + ExitOnFailure(hr, "Failed to load localization."); + + hr = LoadTheme(sczModulePath, _language); + ExitOnFailure(hr, "Failed to load theme."); + + hr = BalInfoParseFromXml(&_bundle, pixdManifest); + BalExitOnFailure(hr, "Failed to load bundle information."); + + hr = BalConditionsParseFromXml(&_conditions, pixdManifest, _wixLoc); + BalExitOnFailure(hr, "Failed to load conditions from XML."); + + hr = LoadBootstrapperBAFunctions(); + BalExitOnFailure(hr, "Failed to load bootstrapper functions."); + hr = UpdateUIStrings(_command.action); + BalExitOnFailure(hr, "Failed to load UI strings."); + + GetBundleFileVersion(); + // don't fail if we couldn't get the version info; best-effort only + LExit: + ReleaseObject(pixdManifest); + ReleaseStr(sczModulePath); + + return hr; + } + + + // + // ProcessCommandLine - process the provided command line arguments. + // + HRESULT ProcessCommandLine(__inout LPWSTR* psczLanguage) { + HRESULT hr = S_OK; + int argc = 0; + LPWSTR* argv = nullptr; + LPWSTR sczVariableName = nullptr; + LPWSTR sczVariableValue = nullptr; + + if (_command.wzCommandLine && *_command.wzCommandLine) { + argv = ::CommandLineToArgvW(_command.wzCommandLine, &argc); + ExitOnNullWithLastError(argv, hr, "Failed to get command line."); + + for (int i = 0; i < argc; ++i) { + if (argv[i][0] == L'-' || argv[i][0] == L'/') { + if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, &argv[i][1], -1, L"lang", -1)) { + if (i + 1 >= argc) { + hr = E_INVALIDARG; + BalExitOnFailure(hr, "Must specify a language."); + } + + ++i; + + hr = StrAllocString(psczLanguage, &argv[i][0], 0); + BalExitOnFailure(hr, "Failed to copy language."); + } else if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, &argv[i][1], -1, L"simple", -1)) { + _engine->SetVariableNumeric(L"SimpleInstall", 1); + } + } else if (_overridableVariables) { + int value; + const wchar_t* pwc = wcschr(argv[i], L'='); + if (pwc) { + hr = StrAllocString(&sczVariableName, argv[i], pwc - argv[i]); + BalExitOnFailure(hr, "Failed to copy variable name."); + + hr = DictKeyExists(_overridableVariables, sczVariableName); + if (E_NOTFOUND == hr) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Ignoring attempt to set non-overridable variable: '%ls'.", sczVariableName); + hr = S_OK; + continue; + } + ExitOnFailure(hr, "Failed to check the dictionary of overridable variables."); + + hr = StrAllocString(&sczVariableValue, ++pwc, 0); + BalExitOnFailure(hr, "Failed to copy variable value."); + + if (::StrToIntEx(sczVariableValue, STIF_DEFAULT, &value)) { + hr = _engine->SetVariableNumeric(sczVariableName, value); + } else { + hr = _engine->SetVariableString(sczVariableName, sczVariableValue); + } + BalExitOnFailure(hr, "Failed to set variable."); + } else { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Ignoring unknown argument: %ls", argv[i]); + } + } + } + } + + LExit: + if (argv) { + ::LocalFree(argv); + } + + ReleaseStr(sczVariableName); + ReleaseStr(sczVariableValue); + + return hr; + } + + HRESULT LoadLocalization(__in_z LPCWSTR wzModulePath, __in_z_opt LPCWSTR wzLanguage) { + HRESULT hr = S_OK; + LPWSTR sczLocPath = nullptr; + LPCWSTR wzLocFileName = L"Default.wxl"; + + hr = LocProbeForFile(wzModulePath, wzLocFileName, wzLanguage, &sczLocPath); + BalExitOnFailure2(hr, "Failed to probe for loc file: %ls in path: %ls", wzLocFileName, wzModulePath); + + hr = LocLoadFromFile(sczLocPath, &_wixLoc); + BalExitOnFailure1(hr, "Failed to load loc file from path: %ls", sczLocPath); + + if (WIX_LOCALIZATION_LANGUAGE_NOT_SET != _wixLoc->dwLangId) { + ::SetThreadLocale(_wixLoc->dwLangId); + } + + hr = StrAllocString(&_confirmCloseMessage, L"#(loc.ConfirmCancelMessage)", 0); + ExitOnFailure(hr, "Failed to initialize confirm message loc identifier."); + + hr = LocLocalizeString(_wixLoc, &_confirmCloseMessage); + BalExitOnFailure1(hr, "Failed to localize confirm close message: %ls", _confirmCloseMessage); + + LExit: + ReleaseStr(sczLocPath); + + return hr; + } + + + HRESULT LoadTheme(__in_z LPCWSTR wzModulePath, __in_z_opt LPCWSTR wzLanguage) { + HRESULT hr = S_OK; + LPWSTR sczThemePath = nullptr; + LPCWSTR wzThemeFileName = L"Default.thm"; + LPWSTR sczCaption = nullptr; + + hr = LocProbeForFile(wzModulePath, wzThemeFileName, wzLanguage, &sczThemePath); + BalExitOnFailure2(hr, "Failed to probe for theme file: %ls in path: %ls", wzThemeFileName, wzModulePath); + + hr = ThemeLoadFromFile(sczThemePath, &_theme); + BalExitOnFailure1(hr, "Failed to load theme from path: %ls", sczThemePath); + + hr = ThemeLocalize(_theme, _wixLoc); + BalExitOnFailure1(hr, "Failed to localize theme: %ls", sczThemePath); + + // Update the caption if there are any formatted strings in it. + // If the wix developer is showing a hidden variable in the UI, then + // obviously they don't care about keeping it safe so don't go down the + // rabbit hole of making sure that this is securely freed. + hr = BalFormatString(_theme->sczCaption, &sczCaption); + if (SUCCEEDED(hr)) { + ThemeUpdateCaption(_theme, sczCaption); + } + + LExit: + ReleaseStr(sczCaption); + ReleaseStr(sczThemePath); + + return hr; + } + + + HRESULT ParseOverridableVariablesFromXml(__in IXMLDOMDocument* pixdManifest) { + HRESULT hr = S_OK; + IXMLDOMNode* pNode = nullptr; + IXMLDOMNodeList* pNodes = nullptr; + DWORD cNodes = 0; + LPWSTR scz = nullptr; + BOOL hidden = FALSE; + + // get the list of variables users can override on the command line + hr = XmlSelectNodes(pixdManifest, L"/BootstrapperApplicationData/WixStdbaOverridableVariable", &pNodes); + if (S_FALSE == hr) { + ExitFunction1(hr = S_OK); + } + ExitOnFailure(hr, "Failed to select overridable variable nodes."); + + hr = pNodes->get_length((long*)&cNodes); + ExitOnFailure(hr, "Failed to get overridable variable node count."); + + if (cNodes) { + hr = DictCreateStringList(&_overridableVariables, 32, DICT_FLAG_NONE); + ExitOnFailure(hr, "Failed to create the string dictionary."); + + for (DWORD i = 0; i < cNodes; ++i) { + hr = XmlNextElement(pNodes, &pNode, nullptr); + ExitOnFailure(hr, "Failed to get next node."); + + // @Name + hr = XmlGetAttributeEx(pNode, L"Name", &scz); + ExitOnFailure(hr, "Failed to get @Name."); + + hr = XmlGetYesNoAttribute(pNode, L"Hidden", &hidden); + + if (!hidden) { + hr = DictAddKey(_overridableVariables, scz); + ExitOnFailure1(hr, "Failed to add \"%ls\" to the string dictionary.", scz); + } + + // prepare next iteration + ReleaseNullObject(pNode); + } + } + + LExit: + ReleaseObject(pNode); + ReleaseObject(pNodes); + ReleaseStr(scz); + return hr; + } + + + // + // Get the file version of the bootstrapper and record in bootstrapper log file + // + HRESULT GetBundleFileVersion() { + HRESULT hr = S_OK; + ULARGE_INTEGER uliVersion = { }; + LPWSTR sczCurrentPath = nullptr; + + hr = PathForCurrentProcess(&sczCurrentPath, nullptr); + BalExitOnFailure(hr, "Failed to get bundle path."); + + hr = FileVersion(sczCurrentPath, &uliVersion.HighPart, &uliVersion.LowPart); + BalExitOnFailure(hr, "Failed to get bundle file version."); + + hr = _engine->SetVariableVersion(PYBA_VARIABLE_BUNDLE_FILE_VERSION, uliVersion.QuadPart); + BalExitOnFailure(hr, "Failed to set WixBundleFileVersion variable."); + + LExit: + ReleaseStr(sczCurrentPath); + + return hr; + } + + + // + // CreateMainWindow - creates the main install window. + // + HRESULT CreateMainWindow() { + HRESULT hr = S_OK; + HICON hIcon = reinterpret_cast<HICON>(_theme->hIcon); + WNDCLASSW wc = { }; + DWORD dwWindowStyle = 0; + int x = CW_USEDEFAULT; + int y = CW_USEDEFAULT; + POINT ptCursor = { }; + HMONITOR hMonitor = nullptr; + MONITORINFO mi = { }; + COLORREF fg, bg; + HBRUSH bgBrush; + + // If the theme did not provide an icon, try using the icon from the bundle engine. + if (!hIcon) { + HMODULE hBootstrapperEngine = ::GetModuleHandleW(nullptr); + if (hBootstrapperEngine) { + hIcon = ::LoadIconW(hBootstrapperEngine, MAKEINTRESOURCEW(1)); + } + } + + fg = RGB(0, 0, 0); + bg = RGB(255, 255, 255); + bgBrush = (HBRUSH)(COLOR_WINDOW+1); + if (_theme->dwFontId < _theme->cFonts) { + THEME_FONT *font = &_theme->rgFonts[_theme->dwFontId]; + fg = font->crForeground; + bg = font->crBackground; + bgBrush = font->hBackground; + RemapColor(&fg, &bg, &bgBrush); + } + + // Register the window class and create the window. + wc.lpfnWndProc = PythonBootstrapperApplication::WndProc; + wc.hInstance = _hModule; + wc.hIcon = hIcon; + wc.hCursor = ::LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); + wc.hbrBackground = bgBrush; + wc.lpszMenuName = nullptr; + wc.lpszClassName = PYBA_WINDOW_CLASS; + if (!::RegisterClassW(&wc)) { + ExitWithLastError(hr, "Failed to register window."); + } + + _registered = TRUE; + + // Calculate the window style based on the theme style and command display value. + dwWindowStyle = _theme->dwStyle; + if (BOOTSTRAPPER_DISPLAY_NONE >= _command.display) { + dwWindowStyle &= ~WS_VISIBLE; + } + + // Don't show the window if there is a splash screen (it will be made visible when the splash screen is hidden) + if (::IsWindow(_command.hwndSplashScreen)) { + dwWindowStyle &= ~WS_VISIBLE; + } + + // Center the window on the monitor with the mouse. + if (::GetCursorPos(&ptCursor)) { + hMonitor = ::MonitorFromPoint(ptCursor, MONITOR_DEFAULTTONEAREST); + if (hMonitor) { + mi.cbSize = sizeof(mi); + if (::GetMonitorInfoW(hMonitor, &mi)) { + x = mi.rcWork.left + (mi.rcWork.right - mi.rcWork.left - _theme->nWidth) / 2; + y = mi.rcWork.top + (mi.rcWork.bottom - mi.rcWork.top - _theme->nHeight) / 2; + } + } + } + + _hWnd = ::CreateWindowExW( + 0, + wc.lpszClassName, + _theme->sczCaption, + dwWindowStyle, + x, + y, + _theme->nWidth, + _theme->nHeight, + HWND_DESKTOP, + nullptr, + _hModule, + this + ); + ExitOnNullWithLastError(_hWnd, hr, "Failed to create window."); + + hr = S_OK; + + LExit: + return hr; + } + + + // + // InitializeTaskbarButton - initializes taskbar button for progress. + // + void InitializeTaskbarButton() { + HRESULT hr = S_OK; + + hr = ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL, __uuidof(ITaskbarList3), reinterpret_cast<LPVOID*>(&_taskbarList)); + if (REGDB_E_CLASSNOTREG == hr) { + // not supported before Windows 7 + ExitFunction1(hr = S_OK); + } + BalExitOnFailure(hr, "Failed to create ITaskbarList3. Continuing."); + + _taskbarButtonCreatedMessage = ::RegisterWindowMessageW(L"TaskbarButtonCreated"); + BalExitOnNullWithLastError(_taskbarButtonCreatedMessage, hr, "Failed to get TaskbarButtonCreated message. Continuing."); + + LExit: + return; + } + + // + // DestroyMainWindow - clean up all the window registration. + // + void DestroyMainWindow() { + if (::IsWindow(_hWnd)) { + ::DestroyWindow(_hWnd); + _hWnd = nullptr; + _taskbarButtonOK = FALSE; + } + + if (_registered) { + ::UnregisterClassW(PYBA_WINDOW_CLASS, _hModule); + _registered = FALSE; + } + } + + + // + // WndProc - standard windows message handler. + // + static LRESULT CALLBACK WndProc( + __in HWND hWnd, + __in UINT uMsg, + __in WPARAM wParam, + __in LPARAM lParam + ) { +#pragma warning(suppress:4312) + auto pBA = reinterpret_cast<PythonBootstrapperApplication*>(::GetWindowLongPtrW(hWnd, GWLP_USERDATA)); + + switch (uMsg) { + case WM_NCCREATE: { + LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam); + pBA = reinterpret_cast<PythonBootstrapperApplication*>(lpcs->lpCreateParams); +#pragma warning(suppress:4244) + ::SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pBA)); + break; + } + + case WM_NCDESTROY: { + LRESULT lres = ThemeDefWindowProc(pBA ? pBA->_theme : nullptr, hWnd, uMsg, wParam, lParam); + ::SetWindowLongPtrW(hWnd, GWLP_USERDATA, 0); + return lres; + } + + case WM_CREATE: + if (!pBA->OnCreate(hWnd)) { + return -1; + } + break; + + case WM_QUERYENDSESSION: + return IDCANCEL != pBA->OnSystemShutdown(static_cast<DWORD>(lParam), IDCANCEL); + + case WM_CLOSE: + // If the user chose not to close, do *not* let the default window proc handle the message. + if (!pBA->OnClose()) { + return 0; + } + break; + + case WM_DESTROY: + ::PostQuitMessage(0); + break; + + case WM_PAINT: __fallthrough; + case WM_ERASEBKGND: + if (pBA && pBA->_suppressPaint) { + return TRUE; + } + break; + + case WM_PYBA_SHOW_HELP: + pBA->OnShowHelp(); + return 0; + + case WM_PYBA_DETECT_PACKAGES: + pBA->OnDetect(); + return 0; + + case WM_PYBA_PLAN_PACKAGES: + pBA->OnPlan(static_cast<BOOTSTRAPPER_ACTION>(lParam)); + return 0; + + case WM_PYBA_APPLY_PACKAGES: + pBA->OnApply(); + return 0; + + case WM_PYBA_CHANGE_STATE: + pBA->OnChangeState(static_cast<PYBA_STATE>(lParam)); + return 0; + + case WM_PYBA_SHOW_FAILURE: + pBA->OnShowFailure(); + return 0; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + // Customize commands + // Success/failure commands + case ID_LAUNCH_BUTTON: + pBA->OnClickLaunchButton(); + return 0; + + case ID_SUCCESS_RESTART_BUTTON: __fallthrough; + case ID_FAILURE_RESTART_BUTTON: + pBA->OnClickRestartButton(); + return 0; + + case IDCANCEL: __fallthrough; + case ID_INSTALL_CANCEL_BUTTON: __fallthrough; + case ID_CUSTOM1_CANCEL_BUTTON: __fallthrough; + case ID_CUSTOM2_CANCEL_BUTTON: __fallthrough; + case ID_MODIFY_CANCEL_BUTTON: __fallthrough; + case ID_PROGRESS_CANCEL_BUTTON: __fallthrough; + case ID_SUCCESS_CANCEL_BUTTON: __fallthrough; + case ID_FAILURE_CANCEL_BUTTON: __fallthrough; + case ID_CLOSE_BUTTON: + pBA->OnCommand(ID_CLOSE_BUTTON); + return 0; + + default: + pBA->OnCommand((CONTROL_ID)LOWORD(wParam)); + } + break; + + case WM_NOTIFY: + if (lParam) { + LPNMHDR pnmhdr = reinterpret_cast<LPNMHDR>(lParam); + switch (pnmhdr->code) { + case NM_CLICK: __fallthrough; + case NM_RETURN: + switch (static_cast<DWORD>(pnmhdr->idFrom)) { + case ID_FAILURE_LOGFILE_LINK: + pBA->OnClickLogFileLink(); + return 1; + } + } + } + break; + + case WM_CTLCOLORSTATIC: + case WM_CTLCOLORBTN: + if (pBA) { + HBRUSH brush = nullptr; + if (pBA->SetControlColor((HWND)lParam, (HDC)wParam, &brush)) { + return (LRESULT)brush; + } + } + break; + } + + if (pBA && pBA->_taskbarList && uMsg == pBA->_taskbarButtonCreatedMessage) { + pBA->_taskbarButtonOK = TRUE; + return 0; + } + + return ThemeDefWindowProc(pBA ? pBA->_theme : nullptr, hWnd, uMsg, wParam, lParam); + } + + // + // OnCreate - finishes loading the theme. + // + BOOL OnCreate(__in HWND hWnd) { + HRESULT hr = S_OK; + + hr = ThemeLoadControls(_theme, hWnd, CONTROL_ID_NAMES, countof(CONTROL_ID_NAMES)); + BalExitOnFailure(hr, "Failed to load theme controls."); + + C_ASSERT(COUNT_PAGE == countof(PAGE_NAMES)); + C_ASSERT(countof(_pageIds) == countof(PAGE_NAMES)); + + ThemeGetPageIds(_theme, PAGE_NAMES, _pageIds, countof(_pageIds)); + + // Initialize the text on all "application" (non-page) controls. + for (DWORD i = 0; i < _theme->cControls; ++i) { + THEME_CONTROL* pControl = _theme->rgControls + i; + LPWSTR text = nullptr; + LPWSTR name = nullptr; + LOC_STRING *locText = nullptr; + + // If a command link has a note, then add it. + if ((pControl->dwStyle & BS_TYPEMASK) == BS_COMMANDLINK || + (pControl->dwStyle & BS_TYPEMASK) == BS_DEFCOMMANDLINK) { + hr = StrAllocFormatted(&name, L"#(loc.%lsNote)", pControl->sczName); + if (SUCCEEDED(hr)) { + hr = LocGetString(_wixLoc, name, &locText); + ReleaseStr(name); + if (SUCCEEDED(hr) && locText && locText->wzText && locText->wzText[0]) { + hr = BalFormatString(locText->wzText, &text); + if (SUCCEEDED(hr) && text && text[0]) { + ThemeSendControlMessage(_theme, pControl->wId, BCM_SETNOTE, 0, (LPARAM)text); + ReleaseStr(text); + text = nullptr; + } + } + } + hr = S_OK; + } + + if (!pControl->wPageId && pControl->sczText && *pControl->sczText) { + HRESULT hrFormat; + + // If the wix developer is showing a hidden variable in the UI, + // then obviously they don't care about keeping it safe so don't + // go down the rabbit hole of making sure that this is securely + // freed. + hrFormat = BalFormatString(pControl->sczText, &text); + if (SUCCEEDED(hrFormat)) { + ThemeSetTextControl(_theme, pControl->wId, text); + ReleaseStr(text); + } + } + } + + LExit: + return SUCCEEDED(hr); + } + + void RemapColor(COLORREF *fg, COLORREF *bg, HBRUSH *bgBrush) { + if (*fg == RGB(0, 0, 0)) { + *fg = GetSysColor(COLOR_WINDOWTEXT); + } else if (*fg == RGB(128, 128, 128)) { + *fg = GetSysColor(COLOR_GRAYTEXT); + } + if (*bgBrush && *bg == RGB(255, 255, 255)) { + *bg = GetSysColor(COLOR_WINDOW); + *bgBrush = GetSysColorBrush(COLOR_WINDOW); + } + } + + BOOL SetControlColor(HWND hWnd, HDC hDC, HBRUSH *brush) { + for (int i = 0; i < _theme->cControls; ++i) { + if (_theme->rgControls[i].hWnd != hWnd) { + continue; + } + + DWORD fontId = _theme->rgControls[i].dwFontId; + if (fontId > _theme->cFonts) { + fontId = 0; + } + THEME_FONT *fnt = &_theme->rgFonts[fontId]; + + COLORREF fg = fnt->crForeground, bg = fnt->crBackground; + *brush = fnt->hBackground; + RemapColor(&fg, &bg, brush); + ::SetTextColor(hDC, fg); + ::SetBkColor(hDC, bg); + + return TRUE; + } + return FALSE; + } + + // + // OnShowFailure - display the failure page. + // + void OnShowFailure() { + SetState(PYBA_STATE_FAILED, S_OK); + + // If the UI should be visible, display it now and hide the splash screen + if (BOOTSTRAPPER_DISPLAY_NONE < _command.display) { + ::ShowWindow(_theme->hwndParent, SW_SHOW); + } + + _engine->CloseSplashScreen(); + + return; + } + + + // + // OnShowHelp - display the help page. + // + void OnShowHelp() { + SetState(PYBA_STATE_HELP, S_OK); + + // If the UI should be visible, display it now and hide the splash screen + if (BOOTSTRAPPER_DISPLAY_NONE < _command.display) { + ::ShowWindow(_theme->hwndParent, SW_SHOW); + } + + _engine->CloseSplashScreen(); + + return; + } + + + // + // OnDetect - start the processing of packages. + // + void OnDetect() { + HRESULT hr = S_OK; + + if (_baFunction) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Running detect BA function"); + hr = _baFunction->OnDetect(); + BalExitOnFailure(hr, "Failed calling detect BA function."); + } + + SetState(PYBA_STATE_DETECTING, hr); + + // If the UI should be visible, display it now and hide the splash screen + if (BOOTSTRAPPER_DISPLAY_NONE < _command.display) { + ::ShowWindow(_theme->hwndParent, SW_SHOW); + } + + _engine->CloseSplashScreen(); + + // Tell the core we're ready for the packages to be processed now. + hr = _engine->Detect(); + BalExitOnFailure(hr, "Failed to start detecting chain."); + + LExit: + if (FAILED(hr)) { + SetState(PYBA_STATE_DETECTING, hr); + } + + return; + } + HRESULT UpdateUIStrings(__in BOOTSTRAPPER_ACTION action) { + HRESULT hr = S_OK; + LPCWSTR likeInstalling = nullptr; + LPCWSTR likeInstallation = nullptr; + switch (action) { + case BOOTSTRAPPER_ACTION_INSTALL: + likeInstalling = L"Installing"; + likeInstallation = L"Installation"; + break; + case BOOTSTRAPPER_ACTION_MODIFY: + // For modify, we actually want to pass INSTALL + action = BOOTSTRAPPER_ACTION_INSTALL; + likeInstalling = L"Modifying"; + likeInstallation = L"Modification"; + break; + case BOOTSTRAPPER_ACTION_REPAIR: + likeInstalling = L"Repairing"; + likeInstallation = L"Repair"; + break; + case BOOTSTRAPPER_ACTION_UNINSTALL: + likeInstalling = L"Uninstalling"; + likeInstallation = L"Uninstallation"; + break; + } + + if (likeInstalling) { + LPWSTR locName = nullptr; + LOC_STRING *locText = nullptr; + hr = StrAllocFormatted(&locName, L"#(loc.%ls)", likeInstalling); + if (SUCCEEDED(hr)) { + hr = LocGetString(_wixLoc, locName, &locText); + ReleaseStr(locName); + } + _engine->SetVariableString( + L"ActionLikeInstalling", + SUCCEEDED(hr) && locText ? locText->wzText : likeInstalling + ); + } + + if (likeInstallation) { + LPWSTR locName = nullptr; + LOC_STRING *locText = nullptr; + hr = StrAllocFormatted(&locName, L"#(loc.%ls)", likeInstallation); + if (SUCCEEDED(hr)) { + hr = LocGetString(_wixLoc, locName, &locText); + ReleaseStr(locName); + } + _engine->SetVariableString( + L"ActionLikeInstallation", + SUCCEEDED(hr) && locText ? locText->wzText : likeInstallation + ); + } + return hr; + } + + // + // OnPlan - plan the detected changes. + // + void OnPlan(__in BOOTSTRAPPER_ACTION action) { + HRESULT hr = S_OK; + + _plannedAction = action; + + hr = UpdateUIStrings(action); + BalExitOnFailure(hr, "Failed to update strings"); + + // If we are going to apply a downgrade, bail. + if (_downgradingOtherVersion && BOOTSTRAPPER_ACTION_UNINSTALL < action) { + if (_suppressDowngradeFailure) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "A newer version of this product is installed but downgrade failure has been suppressed; continuing..."); + } else { + hr = HRESULT_FROM_WIN32(ERROR_PRODUCT_VERSION); + BalExitOnFailure(hr, "Cannot install a product when a newer version is installed."); + } + } + + SetState(PYBA_STATE_PLANNING, hr); + + if (_baFunction) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Running plan BA function"); + _baFunction->OnPlan(); + } + + hr = _engine->Plan(action); + BalExitOnFailure(hr, "Failed to start planning packages."); + + LExit: + if (FAILED(hr)) { + SetState(PYBA_STATE_PLANNING, hr); + } + + return; + } + + + // + // OnApply - apply the packages. + // + void OnApply() { + HRESULT hr = S_OK; + + SetState(PYBA_STATE_APPLYING, hr); + SetProgressState(hr); + SetTaskbarButtonProgress(0); + + hr = _engine->Apply(_hWnd); + BalExitOnFailure(hr, "Failed to start applying packages."); + + ThemeControlEnable(_theme, ID_PROGRESS_CANCEL_BUTTON, TRUE); // ensure the cancel button is enabled before starting. + + LExit: + if (FAILED(hr)) { + SetState(PYBA_STATE_APPLYING, hr); + } + + return; + } + + + // + // OnChangeState - change state. + // + void OnChangeState(__in PYBA_STATE state) { + LPWSTR unformattedText = nullptr; + + _state = state; + + // If our install is at the end (success or failure) and we're not showing full UI + // then exit (prompt for restart if required). + if ((PYBA_STATE_APPLIED <= _state && BOOTSTRAPPER_DISPLAY_FULL > _command.display)) { + // If a restart was required but we were not automatically allowed to + // accept the reboot then do the prompt. + if (_restartRequired && !_allowRestart) { + StrAllocFromError(&unformattedText, HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED), nullptr); + + _allowRestart = IDOK == ::MessageBoxW( + _hWnd, + unformattedText ? unformattedText : L"The requested operation is successful. Changes will not be effective until the system is rebooted.", + _theme->sczCaption, + MB_ICONEXCLAMATION | MB_OKCANCEL + ); + } + + // Quietly exit. + ::PostMessageW(_hWnd, WM_CLOSE, 0, 0); + } else { // try to change the pages. + DWORD newPageId = 0; + DeterminePageId(_state, &newPageId); + + if (_visiblePageId != newPageId) { + ShowPage(newPageId); + } + } + + ReleaseStr(unformattedText); + } + + // + // Called before showing a page to handle all controls. + // + void ProcessPageControls(THEME_PAGE *pPage) { + if (!pPage) { + return; + } + + for (DWORD i = 0; i < pPage->cControlIndices; ++i) { + THEME_CONTROL* pControl = _theme->rgControls + pPage->rgdwControlIndices[i]; + BOOL enableControl = TRUE; + + // If this is a named control, try to set its default state. + if (pControl->sczName && *pControl->sczName) { + // If this is a checkable control, try to set its default state + // to the state of a matching named Burn variable. + if (IsCheckable(pControl)) { + LONGLONG llValue = 0; + HRESULT hr = BalGetNumericVariable(pControl->sczName, &llValue); + + // If the control value isn't set then disable it. + if (!SUCCEEDED(hr)) { + enableControl = FALSE; + } else { + ThemeSendControlMessage( + _theme, + pControl->wId, + BM_SETCHECK, + SUCCEEDED(hr) && llValue ? BST_CHECKED : BST_UNCHECKED, + 0 + ); + } + } + + // Hide or disable controls based on the control name with 'State' appended + LPWSTR controlName = nullptr; + HRESULT hr = StrAllocFormatted(&controlName, L"%lsState", pControl->sczName); + if (SUCCEEDED(hr)) { + LPWSTR controlState = nullptr; + hr = BalGetStringVariable(controlName, &controlState); + if (SUCCEEDED(hr) && controlState && *controlState) { + if (controlState[0] == '[') { + LPWSTR formatted = nullptr; + if (SUCCEEDED(BalFormatString(controlState, &formatted))) { + StrFree(controlState); + controlState = formatted; + } + } + + if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, controlState, -1, L"disable", -1)) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Disable control %ls", pControl->sczName); + enableControl = FALSE; + } else if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, controlState, -1, L"hide", -1)) { + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Hide control %ls", pControl->sczName); + // TODO: This doesn't work + ThemeShowControl(_theme, pControl->wId, SW_HIDE); + } else { + // An explicit state can override the lack of a + // backing variable. + enableControl = TRUE; + } + } + StrFree(controlState); + } + StrFree(controlName); + } + + ThemeControlEnable(_theme, pControl->wId, enableControl); + + // Format the text in each of the new page's controls + if (pControl->sczText && *pControl->sczText) { + // If the wix developer is showing a hidden variable + // in the UI, then obviously they don't care about + // keeping it safe so don't go down the rabbit hole + // of making sure that this is securely freed. + LPWSTR text = nullptr; + HRESULT hr = BalFormatString(pControl->sczText, &text); + if (SUCCEEDED(hr)) { + ThemeSetTextControl(_theme, pControl->wId, text); + } + } + } + } + + // + // OnClose - called when the window is trying to be closed. + // + BOOL OnClose() { + BOOL close = FALSE; + + // If we've already succeeded or failed or showing the help page, just close (prompts are annoying if the bootstrapper is done). + if (PYBA_STATE_APPLIED <= _state || PYBA_STATE_HELP == _state) { + close = TRUE; + } else { + // prompt the user or force the cancel if there is no UI. + close = PromptCancel( + _hWnd, + BOOTSTRAPPER_DISPLAY_FULL != _command.display, + _confirmCloseMessage ? _confirmCloseMessage : L"Are you sure you want to cancel?", + _theme->sczCaption + ); + } + + // If we're doing progress then we never close, we just cancel to let rollback occur. + if (PYBA_STATE_APPLYING <= _state && PYBA_STATE_APPLIED > _state) { + // If we canceled disable cancel button since clicking it again is silly. + if (close) { + ThemeControlEnable(_theme, ID_PROGRESS_CANCEL_BUTTON, FALSE); + } + + close = FALSE; + } + + return close; + } + + // + // OnClickCloseButton - close the application. + // + void OnClickCloseButton() { + ::SendMessageW(_hWnd, WM_CLOSE, 0, 0); + } + + + // + // OnClickLaunchButton - launch the app from the success page. + // + void OnClickLaunchButton() { + HRESULT hr = S_OK; + LPWSTR sczUnformattedLaunchTarget = nullptr; + LPWSTR sczLaunchTarget = nullptr; + LPWSTR sczLaunchTargetElevatedId = nullptr; + LPWSTR sczUnformattedArguments = nullptr; + LPWSTR sczArguments = nullptr; + int nCmdShow = SW_SHOWNORMAL; + + hr = BalGetStringVariable(PYBA_VARIABLE_LAUNCH_TARGET_PATH, &sczUnformattedLaunchTarget); + BalExitOnFailure1(hr, "Failed to get launch target variable '%ls'.", PYBA_VARIABLE_LAUNCH_TARGET_PATH); + + hr = BalFormatString(sczUnformattedLaunchTarget, &sczLaunchTarget); + BalExitOnFailure1(hr, "Failed to format launch target variable: %ls", sczUnformattedLaunchTarget); + + if (BalStringVariableExists(PYBA_VARIABLE_LAUNCH_TARGET_ELEVATED_ID)) { + hr = BalGetStringVariable(PYBA_VARIABLE_LAUNCH_TARGET_ELEVATED_ID, &sczLaunchTargetElevatedId); + BalExitOnFailure1(hr, "Failed to get launch target elevated id '%ls'.", PYBA_VARIABLE_LAUNCH_TARGET_ELEVATED_ID); + } + + if (BalStringVariableExists(PYBA_VARIABLE_LAUNCH_ARGUMENTS)) { + hr = BalGetStringVariable(PYBA_VARIABLE_LAUNCH_ARGUMENTS, &sczUnformattedArguments); + BalExitOnFailure1(hr, "Failed to get launch arguments '%ls'.", PYBA_VARIABLE_LAUNCH_ARGUMENTS); + } + + if (BalStringVariableExists(PYBA_VARIABLE_LAUNCH_HIDDEN)) { + nCmdShow = SW_HIDE; + } + + if (sczLaunchTargetElevatedId && !_triedToLaunchElevated) { + _triedToLaunchElevated = TRUE; + hr = _engine->LaunchApprovedExe(_hWnd, sczLaunchTargetElevatedId, sczUnformattedArguments, 0); + if (FAILED(hr)) { + BalLogError(hr, "Failed to launch elevated target: %ls", sczLaunchTargetElevatedId); + + //try with ShelExec next time + OnClickLaunchButton(); + } + } else { + if (sczUnformattedArguments) { + hr = BalFormatString(sczUnformattedArguments, &sczArguments); + BalExitOnFailure1(hr, "Failed to format launch arguments variable: %ls", sczUnformattedArguments); + } + + hr = ShelExec(sczLaunchTarget, sczArguments, L"open", nullptr, nCmdShow, _hWnd, nullptr); + BalExitOnFailure1(hr, "Failed to launch target: %ls", sczLaunchTarget); + + ::PostMessageW(_hWnd, WM_CLOSE, 0, 0); + } + + LExit: + StrSecureZeroFreeString(sczArguments); + ReleaseStr(sczUnformattedArguments); + ReleaseStr(sczLaunchTargetElevatedId); + StrSecureZeroFreeString(sczLaunchTarget); + ReleaseStr(sczUnformattedLaunchTarget); + + return; + } + + + // + // OnClickRestartButton - allows the restart and closes the app. + // + void OnClickRestartButton() { + AssertSz(_restartRequired, "Restart must be requested to be able to click on the restart button."); + + _allowRestart = TRUE; + ::SendMessageW(_hWnd, WM_CLOSE, 0, 0); + + return; + } + + + // + // OnClickLogFileLink - show the log file. + // + void OnClickLogFileLink() { + HRESULT hr = S_OK; + LPWSTR sczLogFile = nullptr; + + hr = BalGetStringVariable(_bundle.sczLogVariable, &sczLogFile); + BalExitOnFailure1(hr, "Failed to get log file variable '%ls'.", _bundle.sczLogVariable); + + hr = ShelExec(L"notepad.exe", sczLogFile, L"open", nullptr, SW_SHOWDEFAULT, _hWnd, nullptr); + BalExitOnFailure1(hr, "Failed to open log file target: %ls", sczLogFile); + + LExit: + ReleaseStr(sczLogFile); + + return; + } + + + // + // SetState + // + void SetState(__in PYBA_STATE state, __in HRESULT hrStatus) { + if (FAILED(hrStatus)) { + _hrFinal = hrStatus; + } + + if (FAILED(_hrFinal)) { + state = PYBA_STATE_FAILED; + } + + if (_state != state) { + ::PostMessageW(_hWnd, WM_PYBA_CHANGE_STATE, 0, state); + } + } + + // + // GoToPage + // + void GoToPage(__in PAGE page) { + _installPage = page; + ::PostMessageW(_hWnd, WM_PYBA_CHANGE_STATE, 0, _state); + } + + void DeterminePageId(__in PYBA_STATE state, __out DWORD* pdwPageId) { + LONGLONG simple; + + if (BOOTSTRAPPER_DISPLAY_PASSIVE == _command.display) { + switch (state) { + case PYBA_STATE_INITIALIZED: + *pdwPageId = BOOTSTRAPPER_ACTION_HELP == _command.action + ? _pageIds[PAGE_HELP] + : _pageIds[PAGE_LOADING]; + break; + + case PYBA_STATE_HELP: + *pdwPageId = _pageIds[PAGE_HELP]; + break; + + case PYBA_STATE_DETECTING: + *pdwPageId = _pageIds[PAGE_LOADING] + ? _pageIds[PAGE_LOADING] + : _pageIds[PAGE_PROGRESS_PASSIVE] + ? _pageIds[PAGE_PROGRESS_PASSIVE] + : _pageIds[PAGE_PROGRESS]; + break; + + case PYBA_STATE_DETECTED: __fallthrough; + case PYBA_STATE_PLANNING: __fallthrough; + case PYBA_STATE_PLANNED: __fallthrough; + case PYBA_STATE_APPLYING: __fallthrough; + case PYBA_STATE_CACHING: __fallthrough; + case PYBA_STATE_CACHED: __fallthrough; + case PYBA_STATE_EXECUTING: __fallthrough; + case PYBA_STATE_EXECUTED: + *pdwPageId = _pageIds[PAGE_PROGRESS_PASSIVE] + ? _pageIds[PAGE_PROGRESS_PASSIVE] + : _pageIds[PAGE_PROGRESS]; + break; + + default: + *pdwPageId = 0; + break; + } + } else if (BOOTSTRAPPER_DISPLAY_FULL == _command.display) { + switch (state) { + case PYBA_STATE_INITIALIZING: + *pdwPageId = 0; + break; + + case PYBA_STATE_INITIALIZED: + *pdwPageId = BOOTSTRAPPER_ACTION_HELP == _command.action + ? _pageIds[PAGE_HELP] + : _pageIds[PAGE_LOADING]; + break; + + case PYBA_STATE_HELP: + *pdwPageId = _pageIds[PAGE_HELP]; + break; + + case PYBA_STATE_DETECTING: + *pdwPageId = _pageIds[PAGE_LOADING]; + break; + + case PYBA_STATE_DETECTED: + if (_installPage == PAGE_LOADING) { + switch (_command.action) { + case BOOTSTRAPPER_ACTION_INSTALL: + if (_upgradingOldVersion) { + _installPage = PAGE_UPGRADE; + } else if (SUCCEEDED(BalGetNumericVariable(L"SimpleInstall", &simple)) && simple) { + _installPage = PAGE_SIMPLE_INSTALL; + } else { + _installPage = PAGE_INSTALL; + } + break; + + case BOOTSTRAPPER_ACTION_MODIFY: __fallthrough; + case BOOTSTRAPPER_ACTION_REPAIR: __fallthrough; + case BOOTSTRAPPER_ACTION_UNINSTALL: + _installPage = PAGE_MODIFY; + break; + } + } + *pdwPageId = _pageIds[_installPage]; + break; + + case PYBA_STATE_PLANNING: __fallthrough; + case PYBA_STATE_PLANNED: __fallthrough; + case PYBA_STATE_APPLYING: __fallthrough; + case PYBA_STATE_CACHING: __fallthrough; + case PYBA_STATE_CACHED: __fallthrough; + case PYBA_STATE_EXECUTING: __fallthrough; + case PYBA_STATE_EXECUTED: + *pdwPageId = _pageIds[PAGE_PROGRESS]; + break; + + case PYBA_STATE_APPLIED: + *pdwPageId = _pageIds[PAGE_SUCCESS]; + break; + + case PYBA_STATE_FAILED: + *pdwPageId = _pageIds[PAGE_FAILURE]; + break; + } + } + } + + BOOL WillElevate() { + static BAL_CONDITION WILL_ELEVATE_CONDITION = { + L"not WixBundleElevated and (InstallAllUsers or (InstallLauncherAllUsers and Include_launcher))", + L"" + }; + BOOL result; + + return SUCCEEDED(BalConditionEvaluate(&WILL_ELEVATE_CONDITION, _engine, &result, nullptr)) && result; + } + + BOOL IsCrtInstalled() { + if (_crtInstalledToken > 0) { + return TRUE; + } else if (_crtInstalledToken == 0) { + return FALSE; + } + + // Check whether at least CRT v10.0.10137.0 is available. + // It should only be installed as a Windows Update package, which means + // we don't need to worry about 32-bit/64-bit. + LPCWSTR crtFile = L"ucrtbase.dll"; + + DWORD cbVer = GetFileVersionInfoSizeW(crtFile, nullptr); + if (!cbVer) { + _crtInstalledToken = 0; + return FALSE; + } + + void *pData = malloc(cbVer); + if (!pData) { + _crtInstalledToken = 0; + return FALSE; + } + + if (!GetFileVersionInfoW(crtFile, 0, cbVer, pData)) { + free(pData); + _crtInstalledToken = 0; + return FALSE; + } + + VS_FIXEDFILEINFO *ffi; + UINT cb; + BOOL result = FALSE; + + if (VerQueryValueW(pData, L"\\", (LPVOID*)&ffi, &cb) && + ffi->dwFileVersionMS == 0x000A0000 && ffi->dwFileVersionLS >= 0x27990000) { + result = TRUE; + } + + free(pData); + _crtInstalledToken = result ? 1 : 0; + return result; + } + + BOOL QueryElevateForCrtInstall() { + // Called to prompt the user that even though they think they won't need + // to elevate, they actually will because of the CRT install. + if (IsCrtInstalled()) { + // CRT is already installed - no need to prompt + return TRUE; + } + + LONGLONG elevated; + HRESULT hr = BalGetNumericVariable(L"WixBundleElevated", &elevated); + if (SUCCEEDED(hr) && elevated) { + // Already elevated - no need to prompt + return TRUE; + } + + LOC_STRING *locStr; + hr = LocGetString(_wixLoc, L"#(loc.ElevateForCRTInstall)", &locStr); + if (FAILED(hr)) { + BalLogError(hr, "Failed to get ElevateForCRTInstall string"); + return FALSE; + } + return ::MessageBoxW(_hWnd, locStr->wzText, _theme->sczCaption, MB_YESNO) != IDNO; + } + + HRESULT EvaluateConditions() { + HRESULT hr = S_OK; + BOOL result = FALSE; + + for (DWORD i = 0; i < _conditions.cConditions; ++i) { + BAL_CONDITION* pCondition = _conditions.rgConditions + i; + + hr = BalConditionEvaluate(pCondition, _engine, &result, &_failedMessage); + BalExitOnFailure(hr, "Failed to evaluate condition."); + + if (!result) { + // Hope they didn't have hidden variables in their message, because it's going in the log in plaintext. + BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "%ls", _failedMessage); + + hr = E_WIXSTDBA_CONDITION_FAILED; + // todo: remove in WiX v4, in case people are relying on v3.x logging behavior + BalExitOnFailure1(hr, "Bundle condition evaluated to false: %ls", pCondition->sczCondition); + } + } + + ReleaseNullStrSecure(_failedMessage); + + LExit: + return hr; + } + + + void SetTaskbarButtonProgress(__in DWORD dwOverallPercentage) { + HRESULT hr = S_OK; + + if (_taskbarButtonOK) { + hr = _taskbarList->SetProgressValue(_hWnd, dwOverallPercentage, 100UL); + BalExitOnFailure1(hr, "Failed to set taskbar button progress to: %d%%.", dwOverallPercentage); + } + + LExit: + return; + } + + + void SetTaskbarButtonState(__in TBPFLAG tbpFlags) { + HRESULT hr = S_OK; + + if (_taskbarButtonOK) { + hr = _taskbarList->SetProgressState(_hWnd, tbpFlags); + BalExitOnFailure1(hr, "Failed to set taskbar button state.", tbpFlags); + } + + LExit: + return; + } + + + void SetProgressState(__in HRESULT hrStatus) { + TBPFLAG flag = TBPF_NORMAL; + + if (IsCanceled() || HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) == hrStatus) { + flag = TBPF_PAUSED; + } else if (IsRollingBack() || FAILED(hrStatus)) { + flag = TBPF_ERROR; + } + + SetTaskbarButtonState(flag); + } + + + HRESULT LoadBootstrapperBAFunctions() { + HRESULT hr = S_OK; + LPWSTR sczBafPath = nullptr; + + hr = PathRelativeToModule(&sczBafPath, L"bafunctions.dll", _hModule); + BalExitOnFailure(hr, "Failed to get path to BA function DLL."); + +#ifdef DEBUG + BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "PYBA: LoadBootstrapperBAFunctions() - BA function DLL %ls", sczBafPath); +#endif + + _hBAFModule = ::LoadLibraryW(sczBafPath); + if (_hBAFModule) { + auto pfnBAFunctionCreate = reinterpret_cast<PFN_BOOTSTRAPPER_BA_FUNCTION_CREATE>(::GetProcAddress(_hBAFModule, "CreateBootstrapperBAFunction")); + BalExitOnNullWithLastError1(pfnBAFunctionCreate, hr, "Failed to get CreateBootstrapperBAFunction entry-point from: %ls", sczBafPath); + + hr = pfnBAFunctionCreate(_engine, _hBAFModule, &_baFunction); + BalExitOnFailure(hr, "Failed to create BA function."); + } +#ifdef DEBUG + else { + BalLogError(HRESULT_FROM_WIN32(::GetLastError()), "PYBA: LoadBootstrapperBAFunctions() - Failed to load DLL %ls", sczBafPath); + } +#endif + + LExit: + if (_hBAFModule && !_baFunction) { + ::FreeLibrary(_hBAFModule); + _hBAFModule = nullptr; + } + ReleaseStr(sczBafPath); + + return hr; + } + + BOOL IsCheckable(THEME_CONTROL* pControl) { + if (!pControl->sczName || !pControl->sczName[0]) { + return FALSE; + } + + if (pControl->type == THEME_CONTROL_TYPE_CHECKBOX) { + return TRUE; + } + + if (pControl->type == THEME_CONTROL_TYPE_BUTTON) { + if ((pControl->dwStyle & BS_TYPEMASK) == BS_AUTORADIOBUTTON) { + return TRUE; + } + } + + return FALSE; + } + + void SavePageSettings() { + DWORD pageId = 0; + THEME_PAGE* pPage = nullptr; + + DeterminePageId(_state, &pageId); + pPage = ThemeGetPage(_theme, pageId); + if (!pPage) { + return; + } + + for (DWORD i = 0; i < pPage->cControlIndices; ++i) { + // Loop through all the checkable controls and set a Burn variable + // with that name to true or false. + THEME_CONTROL* pControl = _theme->rgControls + pPage->rgdwControlIndices[i]; + if (IsCheckable(pControl) && ThemeControlEnabled(_theme, pControl->wId)) { + BOOL checked = ThemeIsControlChecked(_theme, pControl->wId); + _engine->SetVariableNumeric(pControl->sczName, checked ? 1 : 0); + } + + // Loop through all the editbox controls with names and set a + // Burn variable with that name to the contents. + if (THEME_CONTROL_TYPE_EDITBOX == pControl->type && pControl->sczName && *pControl->sczName) { + LPWSTR sczValue = nullptr; + ThemeGetTextControl(_theme, pControl->wId, &sczValue); + _engine->SetVariableString(pControl->sczName, sczValue); + } + } + } + + static bool IsTargetPlatformx64(__in IBootstrapperEngine* pEngine) { + WCHAR platform[8]; + DWORD platformLen = 8; + + if (FAILED(pEngine->GetVariableString(L"TargetPlatform", platform, &platformLen))) { + return S_FALSE; + } + + return ::CompareStringW(LOCALE_NEUTRAL, 0, platform, -1, L"x64", -1) == CSTR_EQUAL; + } + + static HRESULT LoadOptionalFeatureStatesFromKey( + __in IBootstrapperEngine* pEngine, + __in HKEY hkHive, + __in LPCWSTR subkey + ) { + HKEY hKey; + LRESULT res; + + if (IsTargetPlatformx64(pEngine)) { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_64KEY, &hKey); + } else { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_32KEY, &hKey); + } + if (res == ERROR_FILE_NOT_FOUND) { + return S_FALSE; + } + if (res != ERROR_SUCCESS) { + return HRESULT_FROM_WIN32(res); + } + + for (auto p = OPTIONAL_FEATURES; p->regName; ++p) { + res = RegQueryValueExW(hKey, p->regName, nullptr, nullptr, nullptr, nullptr); + if (res == ERROR_FILE_NOT_FOUND) { + pEngine->SetVariableNumeric(p->variableName, 0); + } else if (res == ERROR_SUCCESS) { + pEngine->SetVariableNumeric(p->variableName, 1); + } else { + RegCloseKey(hKey); + return HRESULT_FROM_WIN32(res); + } + } + + RegCloseKey(hKey); + return S_OK; + } + + static HRESULT LoadTargetDirFromKey( + __in IBootstrapperEngine* pEngine, + __in HKEY hkHive, + __in LPCWSTR subkey + ) { + HKEY hKey; + LRESULT res; + DWORD dataType; + BYTE buffer[1024]; + DWORD bufferLen = sizeof(buffer); + + if (IsTargetPlatformx64(pEngine)) { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_64KEY, &hKey); + } else { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_32KEY, &hKey); + } + if (res == ERROR_FILE_NOT_FOUND) { + return S_FALSE; + } + if (res != ERROR_SUCCESS) { + return HRESULT_FROM_WIN32(res); + } + + res = RegQueryValueExW(hKey, nullptr, nullptr, &dataType, buffer, &bufferLen); + if (res == ERROR_SUCCESS && dataType == REG_SZ && bufferLen < sizeof(buffer)) { + pEngine->SetVariableString(L"TargetDir", reinterpret_cast<wchar_t*>(buffer)); + } + RegCloseKey(hKey); + return HRESULT_FROM_WIN32(res); + } + + static HRESULT LoadLauncherStateFromKey( + __in IBootstrapperEngine* pEngine, + __in HKEY hkHive + ) { + const LPCWSTR subkey = L"Software\\Python\\PyLauncher"; + HKEY hKey; + LRESULT res; + + if (IsTargetPlatformx64(pEngine)) { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_64KEY, &hKey); + } else { + res = RegOpenKeyExW(hkHive, subkey, 0, KEY_READ | KEY_WOW64_32KEY, &hKey); + } + + if (res == ERROR_FILE_NOT_FOUND) { + return S_FALSE; + } + if (res != ERROR_SUCCESS) { + return HRESULT_FROM_WIN32(res); + } + + res = RegQueryValueExW(hKey, nullptr, nullptr, nullptr, nullptr, nullptr); + if (res == ERROR_FILE_NOT_FOUND) { + pEngine->SetVariableNumeric(L"Include_launcher", 0); + } else if (res == ERROR_SUCCESS) { + pEngine->SetVariableNumeric(L"Include_launcher", 1); + } + + res = RegQueryValueExW(hKey, L"AssociateFiles", nullptr, nullptr, nullptr, nullptr); + if (res == ERROR_FILE_NOT_FOUND) { + pEngine->SetVariableNumeric(L"AssociateFiles", 0); + } else if (res == ERROR_SUCCESS) { + pEngine->SetVariableNumeric(L"AssociateFiles", 1); + } + + RegCloseKey(hKey); + return S_OK; + } + + static void LoadOptionalFeatureStates(__in IBootstrapperEngine* pEngine) { + WCHAR subkeyFmt[256]; + WCHAR subkey[256]; + DWORD subkeyLen; + HRESULT hr; + HKEY hkHive; + + // The launcher installation is separate from the Python install, so we + // check its state later. This also checks the file association option. + + // Get the registry key from the bundle, to save having to duplicate it + // in multiple places. + subkeyLen = sizeof(subkeyFmt) / sizeof(subkeyFmt[0]); + hr = pEngine->GetVariableString(L"OptionalFeaturesRegistryKey", subkeyFmt, &subkeyLen); + BalExitOnFailure(hr, "Failed to locate registry key"); + subkeyLen = sizeof(subkey) / sizeof(subkey[0]); + hr = pEngine->FormatString(subkeyFmt, subkey, &subkeyLen); + BalExitOnFailure1(hr, "Failed to format %ls", subkeyFmt); + + // Check the current user's registry for existing features + hkHive = HKEY_CURRENT_USER; + hr = LoadOptionalFeatureStatesFromKey(pEngine, hkHive, subkey); + BalExitOnFailure1(hr, "Failed to read from HKCU\\%ls", subkey); + if (hr == S_FALSE) { + // Now check the local machine registry + hkHive = HKEY_LOCAL_MACHINE; + hr = LoadOptionalFeatureStatesFromKey(pEngine, hkHive, subkey); + BalExitOnFailure1(hr, "Failed to read from HKLM\\%ls", subkey); + if (hr == S_OK) { + // Found a system-wide install, so enable these settings. + pEngine->SetVariableNumeric(L"InstallAllUsers", 1); + pEngine->SetVariableNumeric(L"CompileAll", 1); + } + } + + if (hr == S_OK) { + // Cannot change InstallAllUsersState when upgrading. While there's + // no good reason to not allow installing a per-user and an all-user + // version simultaneously, Burn can't handle the state management + // and will need to uninstall the old one. + pEngine->SetVariableString(L"InstallAllUsersState", L"disable"); + + // Get the previous install directory. This can be changed by the + // user. + subkeyLen = sizeof(subkeyFmt) / sizeof(subkeyFmt[0]); + hr = pEngine->GetVariableString(L"TargetDirRegistryKey", subkeyFmt, &subkeyLen); + BalExitOnFailure(hr, "Failed to locate registry key"); + subkeyLen = sizeof(subkey) / sizeof(subkey[0]); + hr = pEngine->FormatString(subkeyFmt, subkey, &subkeyLen); + BalExitOnFailure1(hr, "Failed to format %ls", subkeyFmt); + LoadTargetDirFromKey(pEngine, hkHive, subkey); + } + + LExit: + return; + } + +public: + // + // Constructor - initialize member variables. + // + PythonBootstrapperApplication( + __in HMODULE hModule, + __in BOOL fPrereq, + __in HRESULT hrHostInitialization, + __in IBootstrapperEngine* pEngine, + __in const BOOTSTRAPPER_COMMAND* pCommand + ) : CBalBaseBootstrapperApplication(pEngine, pCommand, 3, 3000) { + _hModule = hModule; + memcpy_s(&_command, sizeof(_command), pCommand, sizeof(BOOTSTRAPPER_COMMAND)); + + LONGLONG llInstalled = 0; + HRESULT hr = BalGetNumericVariable(L"WixBundleInstalled", &llInstalled); + if (SUCCEEDED(hr) && BOOTSTRAPPER_RESUME_TYPE_REBOOT != _command.resumeType && 0 < llInstalled && BOOTSTRAPPER_ACTION_INSTALL == _command.action) { + _command.action = BOOTSTRAPPER_ACTION_MODIFY; + } else if (0 == llInstalled && (BOOTSTRAPPER_ACTION_MODIFY == _command.action || BOOTSTRAPPER_ACTION_REPAIR == _command.action)) { + _command.action = BOOTSTRAPPER_ACTION_INSTALL; + } + + _plannedAction = BOOTSTRAPPER_ACTION_UNKNOWN; + + + // When resuming from restart doing some install-like operation, try to find the package that forced the + // restart. We'll use this information during planning. + _nextPackageAfterRestart = nullptr; + + if (BOOTSTRAPPER_RESUME_TYPE_REBOOT == _command.resumeType && BOOTSTRAPPER_ACTION_UNINSTALL < _command.action) { + // Ensure the forced restart package variable is null when it is an empty string. + HRESULT hr = BalGetStringVariable(L"WixBundleForcedRestartPackage", &_nextPackageAfterRestart); + if (FAILED(hr) || !_nextPackageAfterRestart || !*_nextPackageAfterRestart) { + ReleaseNullStr(_nextPackageAfterRestart); + } + } + + _crtInstalledToken = -1; + pEngine->SetVariableNumeric(L"CRTInstalled", IsCrtInstalled() ? 1 : 0); + + _wixLoc = nullptr; + memset(&_bundle, 0, sizeof(_bundle)); + memset(&_conditions, 0, sizeof(_conditions)); + _confirmCloseMessage = nullptr; + _failedMessage = nullptr; + + _language = nullptr; + _theme = nullptr; + memset(_pageIds, 0, sizeof(_pageIds)); + _hUiThread = nullptr; + _registered = FALSE; + _hWnd = nullptr; + + _state = PYBA_STATE_INITIALIZING; + _visiblePageId = 0; + _installPage = PAGE_LOADING; + _hrFinal = hrHostInitialization; + + _downgradingOtherVersion = FALSE; + _upgradingOldVersion = FALSE; + _restartResult = BOOTSTRAPPER_APPLY_RESTART_NONE; + _restartRequired = FALSE; + _allowRestart = FALSE; + + _suppressDowngradeFailure = FALSE; + _suppressRepair = FALSE; + _modifying = FALSE; + + _overridableVariables = nullptr; + _taskbarList = nullptr; + _taskbarButtonCreatedMessage = UINT_MAX; + _taskbarButtonOK = FALSE; + _showingInternalUIThisPackage = FALSE; + _triedToLaunchElevated = FALSE; + + _suppressPaint = FALSE; + + pEngine->AddRef(); + _engine = pEngine; + + _hBAFModule = nullptr; + _baFunction = nullptr; + + LoadOptionalFeatureStates(pEngine); + } + + + // + // Destructor - release member variables. + // + ~PythonBootstrapperApplication() { + AssertSz(!::IsWindow(_hWnd), "Window should have been destroyed before destructor."); + AssertSz(!_theme, "Theme should have been released before destructor."); + + ReleaseObject(_taskbarList); + ReleaseDict(_overridableVariables); + ReleaseStr(_failedMessage); + ReleaseStr(_confirmCloseMessage); + BalConditionsUninitialize(&_conditions); + BalInfoUninitialize(&_bundle); + LocFree(_wixLoc); + + ReleaseStr(_language); + ReleaseStr(_nextPackageAfterRestart); + ReleaseNullObject(_engine); + + if (_hBAFModule) { + ::FreeLibrary(_hBAFModule); + _hBAFModule = nullptr; + } + } + +private: + HMODULE _hModule; + BOOTSTRAPPER_COMMAND _command; + IBootstrapperEngine* _engine; + BOOTSTRAPPER_ACTION _plannedAction; + + LPWSTR _nextPackageAfterRestart; + + WIX_LOCALIZATION* _wixLoc; + BAL_INFO_BUNDLE _bundle; + BAL_CONDITIONS _conditions; + LPWSTR _failedMessage; + LPWSTR _confirmCloseMessage; + + LPWSTR _language; + THEME* _theme; + DWORD _pageIds[countof(PAGE_NAMES)]; + HANDLE _hUiThread; + BOOL _registered; + HWND _hWnd; + + PYBA_STATE _state; + HRESULT _hrFinal; + DWORD _visiblePageId; + PAGE _installPage; + + BOOL _startedExecution; + DWORD _calculatedCacheProgress; + DWORD _calculatedExecuteProgress; + + BOOL _downgradingOtherVersion; + BOOL _upgradingOldVersion; + BOOTSTRAPPER_APPLY_RESTART _restartResult; + BOOL _restartRequired; + BOOL _allowRestart; + + BOOL _suppressDowngradeFailure; + BOOL _suppressRepair; + BOOL _modifying; + + int _crtInstalledToken; + + STRINGDICT_HANDLE _overridableVariables; + + ITaskbarList3* _taskbarList; + UINT _taskbarButtonCreatedMessage; + BOOL _taskbarButtonOK; + BOOL _showingInternalUIThisPackage; + BOOL _triedToLaunchElevated; + + BOOL _suppressPaint; + + HMODULE _hBAFModule; + IBootstrapperBAFunction* _baFunction; +}; + +// +// CreateBootstrapperApplication - creates a new IBootstrapperApplication object. +// +HRESULT CreateBootstrapperApplication( + __in HMODULE hModule, + __in BOOL fPrereq, + __in HRESULT hrHostInitialization, + __in IBootstrapperEngine* pEngine, + __in const BOOTSTRAPPER_COMMAND* pCommand, + __out IBootstrapperApplication** ppApplication + ) { + HRESULT hr = S_OK; + + if (fPrereq) { + hr = E_INVALIDARG; + ExitWithLastError(hr, "Failed to create UI thread."); + } + + PythonBootstrapperApplication* pApplication = nullptr; + + pApplication = new PythonBootstrapperApplication(hModule, fPrereq, hrHostInitialization, pEngine, pCommand); + ExitOnNull(pApplication, hr, E_OUTOFMEMORY, "Failed to create new standard bootstrapper application object."); + + *ppApplication = pApplication; + pApplication = nullptr; + +LExit: + ReleaseObject(pApplication); + return hr; +} |