Skip to content

Commit 8ccdf5d

Browse files
Backport PR #25460 on branch 6.x (PR: Elevate User Account Control on Windows if Spyder is installed for all users (Installers)) (#25468)
1 parent 395a92d commit 8ccdf5d

File tree

5 files changed

+250
-43
lines changed

5 files changed

+250
-43
lines changed

spyder/config/base.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ def translate_gettext(x: str) -> str:
637637
def is_conda_based_app(pyexec=sys.executable):
638638
"""
639639
Check if Spyder is running from the conda-based installer by looking for
640-
the `spyder-menu.json` file.
640+
the `conda_based_app` file.
641641
642642
If a Python executable is provided, checks if it is in a conda-based
643643
installer environment or the root environment thereof.
@@ -658,6 +658,20 @@ def is_conda_based_app(pyexec=sys.executable):
658658
return False
659659

660660

661+
def is_installed_all_users():
662+
"""
663+
Check if conda-based installer is installed for all users.
664+
Only for conda-based installers.
665+
"""
666+
real_pyexec = osp.realpath(sys.executable) # may be symlink
667+
668+
if not is_conda_based_app(real_pyexec):
669+
return False
670+
671+
root = real_pyexec.split("envs")[0]
672+
return not osp.exists(root + ".nonadmin")
673+
674+
661675
#==============================================================================
662676
# Reset config files
663677
#==============================================================================
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@rem This script creates/updates the Updater environment and installs Spyder Updater
2+
@echo on
3+
4+
set "conda_exe=%~1" & rem conda executable path
5+
set "conda_cmd=%~2" & rem conda subcommand
6+
set "env_path=%~3" & rem Environment path
7+
set "spy_updater_lock=%~4" & rem Environment lock file
8+
set "spy_updater_conda=%~5" & rem Updater conda package
9+
10+
set "tmpdir=%~ps4"
11+
12+
call :redirect > "%tmpdir%\updater_stdout.log" 2> "%tmpdir%\updater_stderr.log"
13+
14+
:exit
15+
exit /b %errorlevel%
16+
17+
:redirect
18+
@echo on
19+
%conda_exe% %conda_cmd% -q --yes --prefix %env_path% --file "%spy_updater_lock%" || goto :eof
20+
%conda_exe% install -q --yes --prefix %env_path% --no-deps --force-reinstall "%spy_updater_conda%"
21+
goto :eof
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
set -e # Exit if there is an error
3+
4+
conda_exe="$1" # conda executable path
5+
conda_cmd="$2" # conda subcommand
6+
env_path="$3" # Environment path
7+
spy_updater_lock="$4" # Environment lock file
8+
spy_updater_conda="$5" # Updater conda package
9+
10+
tmp_update_dir="$(dirname $spy_updater_lock)"
11+
12+
set -x
13+
"$conda_exe" $conda_cmd -q --yes --prefix "$env_path" --file "$spy_updater_lock"
14+
"$conda_exe" install -q --yes --prefix "$env_path" --no-deps --force-reinstall "$spy_udater_conda"

spyder/plugins/updatemanager/widgets/update.py

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@
1717

1818
# Third-party imports
1919
from qtpy.QtCore import Qt, QThread, QTimer, Signal
20-
from qtpy.QtWidgets import QMessageBox, QWidget, QProgressBar, QPushButton
20+
from qtpy.QtWidgets import (
21+
QGridLayout,
22+
QMessageBox,
23+
QProgressBar,
24+
QPushButton,
25+
QTextEdit,
26+
QWidget,
27+
)
2128
from spyder_kernels.utils.pythonenv import is_conda_env
2229

2330
# Local imports
2431
from spyder import __version__
2532
from spyder.api.config.mixins import SpyderConfigurationAccessor
33+
from spyder.api.fonts import SpyderFontsMixin, SpyderFontType
2634
from spyder.api.translations import _
27-
from spyder.config.base import is_conda_based_app
35+
from spyder.config.base import is_conda_based_app, is_installed_all_users
2836
from spyder.config.gui import is_dark_interface
2937
from spyder.plugins.updatemanager.workers import (
3038
UpdateType,
@@ -36,7 +44,11 @@
3644
from spyder.plugins.updatemanager.utils import get_updater_info
3745
from spyder.utils.conda import find_conda, is_anaconda_pkg
3846
from spyder.utils.palette import SpyderPalette
39-
from spyder.utils.programs import get_temp_dir, is_program_installed
47+
from spyder.utils.programs import (
48+
get_temp_dir,
49+
is_program_installed,
50+
find_program
51+
)
4052
from spyder.widgets.helperwidgets import MessageCheckBox
4153

4254
# Logger setup
@@ -365,7 +377,7 @@ def _start_update_updater(self):
365377
)
366378

367379
self.update_updater_worker.sig_ready.connect(
368-
lambda x: self._start_download() if x else None
380+
self._process_update_updater
369381
)
370382
self.update_updater_worker.sig_ready.connect(
371383
self.update_updater_thread.quit
@@ -379,6 +391,32 @@ def _start_update_updater(self):
379391
)
380392
self.update_updater_thread.start()
381393

394+
def _process_update_updater(self):
395+
"""Process possible errors when updating the updater"""
396+
error = self.update_updater_worker.error
397+
if error is None:
398+
self._start_download()
399+
return
400+
401+
self.set_status(PENDING)
402+
if self.progress_dialog is not None:
403+
self.progress_dialog.accept()
404+
self.progress_dialog = None
405+
406+
if isinstance(error, subprocess.CalledProcessError):
407+
error_msg = _("Error updating Spyder-updater.")
408+
details = [
409+
"*** COMMAND ***",
410+
error.cmd.strip(),
411+
"\n*** STDOUT ***",
412+
error.output.strip(),
413+
"\n*** STDERR ***",
414+
error.stderr.strip(),
415+
]
416+
detailed_error_messagebox(
417+
self, error_msg, details="\n".join(details)
418+
)
419+
382420
def _start_download(self):
383421
"""
384422
Start downloading the installer in a QThread
@@ -612,7 +650,24 @@ def _start_updater(self):
612650
cmd = [updater_path, "--update-info-file", info_file]
613651
if self.restart_spyder:
614652
cmd.append("--start-spyder")
615-
subprocess.Popen(" ".join(cmd), shell=True)
653+
654+
kwargs = dict(shell=True)
655+
if os.name == "nt" and is_installed_all_users():
656+
# Elevate UAC
657+
kwargs.update(executable=find_program("powershell"))
658+
cmd = [
659+
"start",
660+
"-FilePath",
661+
f'"{updater_path}"',
662+
"-ArgumentList",
663+
",".join([f"'{a}'" for a in cmd[1:]]),
664+
"-WindowStyle",
665+
"Hidden",
666+
"-Verb",
667+
"RunAs",
668+
]
669+
670+
subprocess.Popen(" ".join(cmd), **kwargs)
616671

617672

618673
class UpdateMessageBox(QMessageBox):
@@ -622,6 +677,54 @@ def __init__(self, icon=None, text=None, parent=None):
622677
self.setTextFormat(Qt.RichText)
623678

624679

680+
class DetailedUpdateMessageBox(UpdateMessageBox, SpyderFontsMixin):
681+
def __init__(self, icon=None, text=None, parent=None, details=None):
682+
super().__init__(icon=icon, text=text, parent=parent)
683+
self.setSizeGripEnabled(True)
684+
self.details = None
685+
self.setDetailedText(details)
686+
687+
def setDetailedText(self, details=None):
688+
"""
689+
Override setDetailedText.
690+
691+
Note: It is critical that QGridLayout.setRowStretch is called after
692+
QMessageBox.setDetailedText in order for the stretch behavior to work
693+
properly. That is the primary reason for overriding setDetailedText.
694+
"""
695+
if self.details is not None:
696+
self.details.setText(details)
697+
return
698+
699+
super().setDetailedText(details)
700+
self.details = self.findChild(QTextEdit)
701+
702+
self.details.setFont(self.get_font(SpyderFontType.Monospace))
703+
self.details.setLineWrapMode(self.details.NoWrap)
704+
self.details.setMinimumSize(400, 110)
705+
self.details.setLineWrapMode(0)
706+
707+
qgl = self.findChild(QGridLayout)
708+
qgl.setRowStretch(1, 0)
709+
qgl.setRowStretch(3, 100) # QTextEdit should take all the stretch
710+
711+
def event(self, event):
712+
"""Override to allow resizing the dialog when details are visible."""
713+
if event.type() in (event.LayoutRequest, event.Resize):
714+
if event.type() == event.Resize:
715+
result = super().event(event)
716+
else:
717+
result = False
718+
719+
# Allow resize only if details is available and visible.
720+
if self.details and self.details.isVisible():
721+
self.details.setMaximumSize(10000, 10000)
722+
self.setMaximumSize(10000, 10000)
723+
724+
return result
725+
return super().event(event)
726+
727+
625728
class UpdateMessageCheckBox(MessageCheckBox):
626729
def __init__(self, icon=None, text=None, parent=None):
627730
super().__init__(icon=icon, text=text, parent=parent)
@@ -674,8 +777,14 @@ def error_messagebox(parent, error_msg, checkbox=False):
674777
box_class = UpdateMessageCheckBox if checkbox else UpdateMessageBox
675778
box = box_class(icon=QMessageBox.Warning, text=error_msg, parent=parent)
676779
box.setWindowTitle(_("Spyder update error"))
677-
box.setStandardButtons(QMessageBox.Ok)
678-
box.setDefaultButton(QMessageBox.Ok)
780+
box.show()
781+
return box
782+
783+
784+
def detailed_error_messagebox(parent, msg, details):
785+
box = DetailedUpdateMessageBox(
786+
icon=QMessageBox.Warning, text=msg, parent=parent, details=details
787+
)
679788
box.show()
680789
return box
681790

@@ -685,8 +794,6 @@ def info_messagebox(parent, message, version=None, checkbox=False):
685794
message = HEADER.format(version) + message if version else message
686795
box = box_class(icon=QMessageBox.Information, text=message, parent=parent)
687796
box.setWindowTitle(_("New Spyder version"))
688-
box.setStandardButtons(QMessageBox.Ok)
689-
box.setDefaultButton(QMessageBox.Ok)
690797
box.show()
691798
return box
692799

@@ -773,5 +880,4 @@ def manual_update_messagebox(parent, latest_release, channel):
773880
"<br><br>For more information, visit our "
774881
"<a href=\"{}\">installation guide</a>."
775882
).format(URL_I)
776-
777-
info_messagebox(parent, msg)
883+
return info_messagebox(parent, msg)

0 commit comments

Comments
 (0)