Skip to content

Commit 9b7f253

Browse files
authored
gh-115554: Improved logic for handling multiple existing py.exe launcher installs (GH-115793)
1 parent 59167c9 commit 9b7f253

File tree

4 files changed

+140
-74
lines changed

4 files changed

+140
-74
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The installer now has more strict rules about updating the :ref:`launcher`.
2+
In general, most users only have a single launcher installed and will see no
3+
difference. When multiple launchers have been installed, the option to
4+
install the launcher is disabled until all but one have been removed.
5+
Downgrading the launcher (which was never allowed) is now more obviously
6+
blocked.

Tools/msi/bundle/Default.wxl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Select Customize to review current options.</String>
8888
<String Id="InstallAllUsersLabel">Install Python [ShortVersion] for &amp;all users</String>
8989
<String Id="InstallLauncherAllUsersLabel">for &amp;all users (requires admin privileges)</String>
9090
<String Id="ShortInstallLauncherAllUsersLabel">Use admin privi&amp;leges when installing py.exe</String>
91+
<String Id="ShortInstallLauncherBlockedLabel">Python Launcher is already installed</String>
9192
<String Id="PrecompileLabel">&amp;Precompile standard library</String>
9293
<String Id="Include_symbolsLabel">Download debugging &amp;symbols</String>
9394
<String Id="Include_debugLabel">Download debu&amp;g binaries (requires VS 2017 or later)</String>

Tools/msi/bundle/bootstrap/PythonBootstrapperApplication.cpp

Lines changed: 128 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,14 @@ class PythonBootstrapperApplication : public CBalBaseBootstrapperApplication {
442442
ThemeControlElevates(_theme, ID_INSTALL_BUTTON, elevated);
443443
ThemeControlElevates(_theme, ID_INSTALL_SIMPLE_BUTTON, elevated);
444444
ThemeControlElevates(_theme, ID_INSTALL_UPGRADE_BUTTON, elevated);
445+
446+
LONGLONG blockedLauncher;
447+
if (SUCCEEDED(BalGetNumericVariable(L"BlockedLauncher", &blockedLauncher)) && blockedLauncher) {
448+
LOC_STRING *pLocString = nullptr;
449+
if (SUCCEEDED(LocGetString(_wixLoc, L"#(loc.ShortInstallLauncherBlockedLabel)", &pLocString)) && pLocString) {
450+
ThemeSetTextControl(_theme, ID_INSTALL_LAUNCHER_ALL_USERS_CHECKBOX, pLocString->wzText);
451+
}
452+
}
445453
}
446454

447455
void Custom1Page_Show() {
@@ -718,25 +726,67 @@ class PythonBootstrapperApplication : public CBalBaseBootstrapperApplication {
718726
__in DWORD64 /*dw64Version*/,
719727
__in BOOTSTRAPPER_RELATED_OPERATION operation
720728
) {
721-
if (BOOTSTRAPPER_RELATED_OPERATION_MAJOR_UPGRADE == operation &&
722-
(CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, L"launcher_AllUsers", -1) ||
723-
CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, L"launcher_JustForMe", -1))) {
724-
auto hr = LoadAssociateFilesStateFromKey(_engine, fPerMachine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER);
725-
if (hr == S_OK) {
726-
_engine->SetVariableNumeric(L"AssociateFiles", 1);
727-
} else if (hr == S_FALSE) {
728-
_engine->SetVariableNumeric(L"AssociateFiles", 0);
729-
} else if (FAILED(hr)) {
730-
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Failed to load AssociateFiles state: error code 0x%08X", hr);
729+
// Only check launcher_AllUsers because we'll find the same packages
730+
// twice if we check launcher_JustForMe as well.
731+
if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, L"launcher_AllUsers", -1)) {
732+
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "Detected existing launcher install");
733+
734+
LONGLONG blockedLauncher, detectedLauncher;
735+
if (FAILED(BalGetNumericVariable(L"BlockedLauncher", &blockedLauncher))) {
736+
blockedLauncher = 0;
737+
}
738+
739+
// Get the prior DetectedLauncher value so we can see if we've
740+
// detected more than one, and then update the stored variable
741+
// (we use the original value later on via the local).
742+
if (FAILED(BalGetNumericVariable(L"DetectedLauncher", &detectedLauncher))) {
743+
detectedLauncher = 0;
744+
}
745+
if (!detectedLauncher) {
746+
_engine->SetVariableNumeric(L"DetectedLauncher", 1);
747+
}
748+
749+
if (blockedLauncher) {
750+
// Nothing else to do, we're already blocking
751+
}
752+
else if (BOOTSTRAPPER_RELATED_OPERATION_DOWNGRADE == operation) {
753+
// Found a higher version, so we can't install ours.
754+
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Higher version launcher has been detected.");
755+
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Launcher will not be installed");
756+
_engine->SetVariableNumeric(L"BlockedLauncher", 1);
757+
}
758+
else if (detectedLauncher) {
759+
if (!blockedLauncher) {
760+
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Multiple launcher installs have been detected.");
761+
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "No launcher will be installed or upgraded until one has been removed.");
762+
_engine->SetVariableNumeric(L"BlockedLauncher", 1);
763+
}
731764
}
765+
else if (BOOTSTRAPPER_RELATED_OPERATION_MAJOR_UPGRADE == operation) {
766+
// Found an older version, so let's run the equivalent as an upgrade
767+
// This overrides "unknown" all users options, but will leave alone
768+
// any that have already been set/detected.
769+
// User can deselect the option to include the launcher, but cannot
770+
// change it from the current per user/machine setting.
771+
LONGLONG includeLauncher, includeLauncherAllUsers;
772+
if (FAILED(BalGetNumericVariable(L"Include_launcher", &includeLauncher))) {
773+
includeLauncher = -1;
774+
}
775+
if (FAILED(BalGetNumericVariable(L"InstallLauncherAllUsers", &includeLauncherAllUsers))) {
776+
includeLauncherAllUsers = -1;
777+
}
732778

733-
LONGLONG includeLauncher;
734-
if (FAILED(BalGetNumericVariable(L"Include_launcher", &includeLauncher))
735-
|| includeLauncher == -1) {
736-
_engine->SetVariableNumeric(L"Include_launcher", 1);
737-
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", fPerMachine ? 1 : 0);
779+
if (includeLauncher < 0) {
780+
_engine->SetVariableNumeric(L"Include_launcher", 1);
781+
}
782+
if (includeLauncherAllUsers < 0) {
783+
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", fPerMachine ? 1 : 0);
784+
} else if (includeLauncherAllUsers != fPerMachine ? 1 : 0) {
785+
// Requested AllUsers option is inconsistent, so block
786+
_engine->SetVariableNumeric(L"BlockedLauncher", 1);
787+
}
788+
_engine->SetVariableNumeric(L"DetectedOldLauncher", 1);
738789
}
739-
_engine->SetVariableNumeric(L"DetectedOldLauncher", 1);
740790
}
741791
return CheckCanceled() ? IDCANCEL : IDNOACTION;
742792
}
@@ -784,48 +834,7 @@ class PythonBootstrapperApplication : public CBalBaseBootstrapperApplication {
784834
__in LPCWSTR wzPackageId,
785835
__in HRESULT hrStatus,
786836
__in BOOTSTRAPPER_PACKAGE_STATE state
787-
) {
788-
if (FAILED(hrStatus)) {
789-
return;
790-
}
791-
792-
BOOL detectedLauncher = FALSE;
793-
HKEY hkey = HKEY_LOCAL_MACHINE;
794-
if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, L"launcher_AllUsers", -1)) {
795-
if (BOOTSTRAPPER_PACKAGE_STATE_PRESENT == state || BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE == state) {
796-
detectedLauncher = TRUE;
797-
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", 1);
798-
}
799-
} else if (CSTR_EQUAL == ::CompareStringW(LOCALE_NEUTRAL, 0, wzPackageId, -1, L"launcher_JustForMe", -1)) {
800-
if (BOOTSTRAPPER_PACKAGE_STATE_PRESENT == state || BOOTSTRAPPER_PACKAGE_STATE_OBSOLETE == state) {
801-
detectedLauncher = TRUE;
802-
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", 0);
803-
}
804-
}
805-
806-
LONGLONG includeLauncher;
807-
if (SUCCEEDED(BalGetNumericVariable(L"Include_launcher", &includeLauncher))
808-
&& includeLauncher != -1) {
809-
detectedLauncher = FALSE;
810-
}
811-
812-
if (detectedLauncher) {
813-
/* When we detect the current version of the launcher. */
814-
_engine->SetVariableNumeric(L"Include_launcher", 1);
815-
_engine->SetVariableNumeric(L"DetectedLauncher", 1);
816-
_engine->SetVariableString(L"Include_launcherState", L"disable");
817-
_engine->SetVariableString(L"InstallLauncherAllUsersState", L"disable");
818-
819-
auto hr = LoadAssociateFilesStateFromKey(_engine, hkey);
820-
if (hr == S_OK) {
821-
_engine->SetVariableNumeric(L"AssociateFiles", 1);
822-
} else if (hr == S_FALSE) {
823-
_engine->SetVariableNumeric(L"AssociateFiles", 0);
824-
} else if (FAILED(hr)) {
825-
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Failed to load AssociateFiles state: error code 0x%08X", hr);
826-
}
827-
}
828-
}
837+
) { }
829838

830839

831840
virtual STDMETHODIMP_(void) OnDetectComplete(__in HRESULT hrStatus) {
@@ -835,19 +844,67 @@ class PythonBootstrapperApplication : public CBalBaseBootstrapperApplication {
835844
}
836845

837846
if (SUCCEEDED(hrStatus)) {
838-
LONGLONG includeLauncher;
839-
if (SUCCEEDED(BalGetNumericVariable(L"Include_launcher", &includeLauncher))
840-
&& includeLauncher == -1) {
841-
if (BOOTSTRAPPER_ACTION_LAYOUT == _command.action ||
842-
(BOOTSTRAPPER_ACTION_INSTALL == _command.action && !_upgrading)) {
843-
// When installing/downloading, we want to include the launcher
844-
// by default.
845-
_engine->SetVariableNumeric(L"Include_launcher", 1);
846-
} else {
847-
// Any other action, if we didn't detect the MSI then we want to
848-
// keep it excluded
849-
_engine->SetVariableNumeric(L"Include_launcher", 0);
850-
_engine->SetVariableNumeric(L"AssociateFiles", 0);
847+
// Update launcher install states
848+
// If we didn't detect any existing installs, Include_launcher and
849+
// InstallLauncherAllUsers will both be -1, so we will set to their
850+
// defaults and leave the options enabled.
851+
// Otherwise, if we detected an existing install, we disable the
852+
// options so they remain fixed.
853+
// The code in OnDetectRelatedMsiPackage is responsible for figuring
854+
// out whether existing installs are compatible with the settings in
855+
// place during detection.
856+
LONGLONG blockedLauncher;
857+
if (SUCCEEDED(BalGetNumericVariable(L"BlockedLauncher", &blockedLauncher))
858+
&& blockedLauncher) {
859+
_engine->SetVariableNumeric(L"Include_launcher", 0);
860+
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", 0);
861+
_engine->SetVariableString(L"InstallLauncherAllUsersState", L"disable");
862+
_engine->SetVariableString(L"Include_launcherState", L"disable");
863+
}
864+
else {
865+
LONGLONG includeLauncher, includeLauncherAllUsers, associateFiles;
866+
867+
if (FAILED(BalGetNumericVariable(L"Include_launcher", &includeLauncher))) {
868+
includeLauncher = -1;
869+
}
870+
if (FAILED(BalGetNumericVariable(L"InstallLauncherAllUsers", &includeLauncherAllUsers))) {
871+
includeLauncherAllUsers = -1;
872+
}
873+
if (FAILED(BalGetNumericVariable(L"AssociateFiles", &associateFiles))) {
874+
associateFiles = -1;
875+
}
876+
877+
if (includeLauncherAllUsers < 0) {
878+
includeLauncherAllUsers = 0;
879+
_engine->SetVariableNumeric(L"InstallLauncherAllUsers", includeLauncherAllUsers);
880+
}
881+
882+
if (includeLauncher < 0) {
883+
if (BOOTSTRAPPER_ACTION_LAYOUT == _command.action ||
884+
(BOOTSTRAPPER_ACTION_INSTALL == _command.action && !_upgrading)) {
885+
// When installing/downloading, we include the launcher
886+
// (though downloads should ignore this setting anyway)
887+
_engine->SetVariableNumeric(L"Include_launcher", 1);
888+
} else {
889+
// Any other action, we should have detected an existing
890+
// install (e.g. on remove/modify), so if we didn't, we
891+
// assume it's not selected.
892+
_engine->SetVariableNumeric(L"Include_launcher", 0);
893+
_engine->SetVariableNumeric(L"AssociateFiles", 0);
894+
}
895+
}
896+
897+
if (associateFiles < 0) {
898+
auto hr = LoadAssociateFilesStateFromKey(
899+
_engine,
900+
includeLauncherAllUsers ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER
901+
);
902+
if (FAILED(hr)) {
903+
BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Failed to load AssociateFiles state: error code 0x%08X", hr);
904+
} else if (hr == S_OK) {
905+
associateFiles = 1;
906+
}
907+
_engine->SetVariableNumeric(L"AssociateFiles", associateFiles);
851908
}
852909
}
853910
}

Tools/msi/bundle/bundle.wxs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828

2929
<Variable Name="InstallAllUsers" Value="0" bal:Overridable="yes" />
3030
<?if "$(var.PyTestExt)"="" ?>
31-
<Variable Name="InstallLauncherAllUsers" Value="0" bal:Overridable="yes" />
31+
<Variable Name="InstallLauncherAllUsers" Value="-1" bal:Overridable="yes" />
3232
<?else ?>
33-
<Variable Name="InstallLauncherAllUsers" Value="0" />
33+
<Variable Name="InstallLauncherAllUsers" Value="-1" />
3434
<?endif ?>
35+
3536
<Variable Name="TargetDir" Value="" bal:Overridable="yes" />
3637
<?if $(var.Platform)~="x64" ?>
3738
<Variable Name="DefaultAllUsersTargetDir" Value="[ProgramFiles64Folder]Python[WinVerNoDot]" bal:Overridable="yes" />
@@ -91,10 +92,11 @@
9192
<?endif ?>
9293

9394
<Variable Name="LauncherOnly" Value="0" bal:Overridable="yes" />
95+
<Variable Name="BlockedLauncher" Value="0" />
9496
<Variable Name="DetectedLauncher" Value="0" />
9597
<Variable Name="DetectedOldLauncher" Value="0" />
9698

97-
<Variable Name="AssociateFiles" Value="1" bal:Overridable="yes" />
99+
<Variable Name="AssociateFiles" Value="-1" bal:Overridable="yes" />
98100
<Variable Name="Shortcuts" Value="1" bal:Overridable="yes" />
99101
<Variable Name="PrependPath" Value="0" bal:Overridable="yes" />
100102
<Variable Name="AppendPath" Value="0" bal:Overridable="yes" />

0 commit comments

Comments
 (0)