diff --git a/certbot-ci/windows_installer_integration_tests/test_main.py b/certbot-ci/windows_installer_integration_tests/test_main.py index ab08d07f8..d2258b027 100644 --- a/certbot-ci/windows_installer_integration_tests/test_main.py +++ b/certbot-ci/windows_installer_integration_tests/test_main.py @@ -55,8 +55,8 @@ def github_mock(installer): pass class GitHubMock(BaseHTTPRequestHandler): - # def log_message(self, log_format, *args): - # pass + def log_message(self, log_format, *args): + pass def do_GET(self): if re.match(r'^.*/releases/latest$', self.path): @@ -95,8 +95,8 @@ def github_mock(installer): server.server_close() -@pytest.fixture(autouse=True) -def registry_config(signing_cert, github_mock): +@pytest.fixture +def upgrade_env(signing_cert, github_mock): try: _ps('New-Item -Path HKLM:\\Software -Name Certbot -ErrorAction SilentlyContinue | Out-Null; exit 0') _ps('New-ItemProperty -Path HKLM:\\Software\\Certbot -Name CertbotUpgradeApiURL -Value {} ' @@ -105,62 +105,76 @@ def registry_config(signing_cert, github_mock): '([Convert]::ToBase64String((Get-PfxCertificate -FilePath {0}).GetPublicKey())) ' '| Out-Null'.format(signing_cert)) - yield + yield True finally: _ps('Remove-ItemProperty -Path HKLM:\\Software\\Certbot -Name CertbotUpgradeApiURL') _ps('Remove-ItemProperty -Path HKLM:\\Software\\Certbot -Name CertbotSigningPubKey') @unittest.skipIf(os.name != 'nt', reason='Windows installer tests must be run on Windows.') -def test_it(installer): +def test_base(installer): + _assert_certbot_is_broken() + + # Install certbot + subprocess.check_output([installer, '/S']) + + # Assert certbot is installed and runnable + output = subprocess.check_output('certbot --version', shell=True, universal_newlines=True) + assert re.match(r'^certbot \d+\.\d+\.\d+.*$', output), 'Flag --version does not output a version.' + + # Assert the renew + auto-upgrade task is installed and ready + output = _ps('(Get-ScheduledTask -TaskName "{}").State'.format(SCHEDULED_TASK_NAME), capture_stdout=True) + assert output.strip() == 'Ready' + + # Trigger the renew + auto-upgrade task, expecting Certbot to check for certificate renewals. + now = time.time() + _ps('Start-ScheduledTask -TaskName "{}"'.format(SCHEDULED_TASK_NAME)) + _wait_for_task_completion() + + log_path = os.path.join('C:\\', 'Certbot', 'log', 'letsencrypt.log') + + modification_time = os.path.getmtime(log_path) + assert now < modification_time, 'Certbot log file has not been modified by the renew task.' + + with open(log_path) as file_h: + data = file_h.read() + assert 'no renewal failures' in data, 'Renew task did not execute properly.' + + +# NB: This test must sit after test_base, because it needs to have +# a working installation of Certbot, and test_base does that. +@unittest.skipIf(os.name != 'nt', reason='Windows installer tests must be run on Windows.') +def test_upgrade(upgrade_env): + assert upgrade_env + subprocess.check_output(['certbot', '--version']) + + # Break on purpose certbot + _ps('Remove-Item "${env:ProgramFiles(x86)}\\Certbot\\bin\\certbot.exe" -Confirm:$false') + _assert_certbot_is_broken() + + # Trigger the renew + auto-upgrade task, expecting Certbot to be reinstalled and functional again. + now = time.time() + _ps('Start-ScheduledTask -TaskName "{}"'.format(SCHEDULED_TASK_NAME)) + _wait_for_task_completion() + + subprocess.check_output(['certbot', '--version']) + + +def _assert_certbot_is_broken(): try: - subprocess.check_call('certbot --version', shell=True) + subprocess.check_output(['certbot', '--version']) except (subprocess.CalledProcessError, OSError): pass else: raise AssertionError('Expect certbot to not be available in the PATH.') - try: - # Install certbot - subprocess.check_call([installer, '/S']) - # Assert certbot is installed and runnable - output = subprocess.check_output('certbot --version', shell=True, universal_newlines=True) - assert re.match(r'^certbot \d+\.\d+\.\d+.*$', output), 'Flag --version does not output a version.' - - # Assert renew task is installed and ready - output = _ps('(Get-ScheduledTask -TaskName "{}").State'.format(SCHEDULED_TASK_NAME), capture_stdout=True) - assert output.strip() == 'Ready' - - # Assert renew task is working - now = time.time() - _ps('Start-ScheduledTask -TaskName "{}"'.format(SCHEDULED_TASK_NAME)) - - status = 'Running' - while status != 'Ready': - status = _ps('(Get-ScheduledTask -TaskName "{}").State' - .format(SCHEDULED_TASK_NAME), capture_stdout=True).strip() - time.sleep(1) - - log_path = os.path.join('C:\\', 'Certbot', 'log', 'letsencrypt.log') - - modification_time = os.path.getmtime(log_path) - assert now < modification_time, 'Certbot log file has not been modified by the renew task.' - - with open(log_path) as file_h: - data = file_h.read() - assert 'no renewal failures' in data, 'Renew task did not execute properly.' - - finally: - # Sadly this command cannot work in non interactive mode: uninstaller will ask explicitly permission in an UAC prompt - # print('Uninstalling Certbot ...') - # uninstall_path = _ps('(gci "HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"' - # ' | foreach { gp $_.PSPath }' - # ' | ? { $_ -match "Certbot" }' - # ' | select UninstallString)' - # '.UninstallString', capture_stdout=True) - # subprocess.check_call([uninstall_path, '/S']) - pass +def _wait_for_task_completion(): + status = 'Running' + while status != 'Ready': + status = _ps('(Get-ScheduledTask -TaskName "{}").State' + .format(SCHEDULED_TASK_NAME), capture_stdout=True).strip() + time.sleep(1) def _ps(powershell_str, capture_stdout=False): diff --git a/windows-installer/auto-update.ps1 b/windows-installer/auto-update.ps1 index a47f8b13b..4df728cb0 100644 --- a/windows-installer/auto-update.ps1 +++ b/windows-installer/auto-update.ps1 @@ -112,11 +112,11 @@ Aborting auto-upgrade process. if (Test-Path $installDir\uninstall.exe) { # Uninstall old Certbot first Write-Message "Running the uninstaller for old version (install dir: $installDir) ..." - Start-Process -FilePath $installDir\uninstall.exe -ArgumentList "/S _?=$installDir" + Start-Process -FilePath $installDir\uninstall.exe -ArgumentList "/S" -Wait } # Install new version of Certbot Write-Message "Running the installer for new version (install dir: $installDir) ..." - Start-Process -FilePath $installerPath -ArgumentList "/S /D=$installDir" + Start-Process -FilePath $installerPath -ArgumentList "/S /D=$installDir" -Wait Write-Message "Certbot $latestVersion is installed." } catch { diff --git a/windows-installer/tasks-up.ps1 b/windows-installer/tasks-up.ps1 index b669a7cc1..2d1ad54f5 100644 --- a/windows-installer/tasks-up.ps1 +++ b/windows-installer/tasks-up.ps1 @@ -11,7 +11,7 @@ $down = Join-Path (Get-ScriptDirectory) 'tasks-down.ps1' $taskName = "Certbot Renew & Auto-Update Task" $taskDescription = "Execute twice a day the 'certbot renew' command, to renew managed certificates if needed, and upgrade Certbot is a new version is available." -$actionRenew = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -Command "certbot renew"' +$actionRenew = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument "-NoProfile -WindowStyle Hidden -Command ""& '$InstallDir\bin\certbot.exe' renew""" $actionUpgrade = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument "-NoProfile -WindowStyle Hidden -File ""$InstallDir\auto-update.ps1""" $delay = New-TimeSpan -Hours 12 diff --git a/windows-installer/template-nsi.patch b/windows-installer/template-nsi.patch new file mode 100644 index 000000000..fbff99651 Binary files /dev/null and b/windows-installer/template-nsi.patch differ diff --git a/windows-installer/template.nsi b/windows-installer/template.nsi index 024346f71..822e3282a 100644 --- a/windows-installer/template.nsi +++ b/windows-installer/template.nsi @@ -1,7 +1,12 @@ -; This NSIS template is based on the built-in one in pynsist 2.3. -; Added lines are enclosed within "CERTBOT CUSTOM BEGIN/END" comments. -; If pynsist is upgraded, this template must be updated if necessary using the new built-in one. +; This NSIS template is based on the built-in one in pynsist 2.4. +; If pynsist is upgraded, this template may be updated if necessary using the new built-in one. ; Original file can be found here: https://github.com/takluyver/pynsist/blob/2.4/nsist/pyapp.nsi +; Diff file is located at .\template-nsi.patch + +; Require the installer do be installed with admin privileges +RequestExecutionLevel admin +; Set default installation path (overridable with /D= flag) +InstallDir "$PROGRAMFILES\Certbot" !define PRODUCT_NAME "[[ib.appname]]" !define PRODUCT_VERSION "[[ib.version]]" @@ -11,31 +16,9 @@ !define ARCH_TAG "[[arch_tag]]" !define INSTALLER_NAME "[[ib.installer_name]]" !define PRODUCT_ICON "[[icon]]" - -; Marker file to tell the uninstaller that it's a user installation -!define USER_INSTALL_MARKER _user_install_marker SetCompressor lzma -; CERTBOT CUSTOM BEGIN -; Administrator privileges are required to insert a new task in Windows Scheduler. -; Also comment out some options to disable ability to choose AllUsers/CurrentUser install mode. -; As a result, installer run always with admin privileges (because of MULTIUSER_EXECUTIONLEVEL), -; using the AllUsers installation mode by default (because of MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER -; not set), and this default behavior cannot be overridden (because of MULTIUSER_MUI not set). -; See https://nsis.sourceforge.io/Docs/MultiUser/Readme.html -!define MULTIUSER_EXECUTIONLEVEL Admin -;!define MULTIUSER_EXECUTIONLEVEL Highest -;!define MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER -;!define MULTIUSER_MUI -;!define MULTIUSER_INSTALLMODE_COMMANDLINE -; CERTBOT CUSTOM END -!define MULTIUSER_INSTALLMODE_INSTDIR "[[ib.appname]]" -[% if ib.py_bitness == 64 %] -!define MULTIUSER_INSTALLMODE_FUNCTION correct_prog_files -[% endif %] -!include MultiUser.nsh - [% block modernui %] ; Modern UI installer stuff !include "MUI2.nsh" @@ -49,10 +32,6 @@ SetCompressor lzma [% if license_file %] !insertmacro MUI_PAGE_LICENSE [[license_file]] [% endif %] -; CERTBOT CUSTOM BEGIN -; Disable the installation mode page (AllUsers/CurrentUser) -;!insertmacro MULTIUSER_PAGE_INSTALLMODE -; CERTBOT CUSTOM END !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH @@ -60,10 +39,7 @@ SetCompressor lzma !insertmacro MUI_LANGUAGE "English" [% endblock modernui %] -; CERTBOT CUSTOM BEGIN Name "${PRODUCT_NAME} (beta) ${PRODUCT_VERSION}" -;Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" -; CERTBOT CUSTOM END OutFile "${INSTALLER_NAME}" ShowInstDetails show @@ -76,18 +52,13 @@ SectionEnd Section "!${PRODUCT_NAME}" sec_app SetRegView [[ib.py_bitness]] + SetShellVarContext all SectionIn RO File ${PRODUCT_ICON} SetOutPath "$INSTDIR\pkgs" File /r "pkgs\*.*" SetOutPath "$INSTDIR" - ; Marker file for per-user install - StrCmp $MultiUser.InstallMode CurrentUser 0 +3 - FileOpen $0 "$INSTDIR\${USER_INSTALL_MARKER}" w - FileClose $0 - SetFileAttributes "$INSTDIR\${USER_INSTALL_MARKER}" HIDDEN - [% block install_files %] ; Install files [% for destination, group in grouped_files %] @@ -129,14 +100,8 @@ Section "!${PRODUCT_NAME}" sec_app DetailPrint "Setting up command-line launchers..." nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_assemble_launchers.py" [[ python ]] "$INSTDIR\bin"' - StrCmp $MultiUser.InstallMode CurrentUser 0 AddSysPathSystem - ; Add to PATH for current user - nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" add_user "$INSTDIR\bin"' - GoTo AddedSysPath - AddSysPathSystem: - ; Add to PATH for all users - nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" add "$INSTDIR\bin"' - AddedSysPath: + ; Add to PATH for all users + nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" add "$INSTDIR\bin"' [% endif %] [% endblock install_commands %] @@ -164,11 +129,9 @@ Section "!${PRODUCT_NAME}" sec_app WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ "NoRepair" 1 - ; CERTBOT CUSTOM BEGIN ; Execute ps script to create the certbot renew & auto-update task DetailPrint "Setting up certbot renew & auto-update scheduled task" nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\tasks-up.ps1" -InstallDir "$INSTDIR"' - ; CERTBOT CUSTOM END ; Check if we need to reboot IfRebootFlag 0 noreboot @@ -179,21 +142,16 @@ Section "!${PRODUCT_NAME}" sec_app SectionEnd Section "Uninstall" - ; CERTBOT CUSTOM BEGIN + SetRegView [[ib.py_bitness]] + SetShellVarContext all + ; Execute ps script to remove the certbot renew & auto-update task, then delete scripts nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\tasks-down.ps1"' Delete "$INSTDIR\tasks-down.ps1" Delete "$INSTDIR\tasks-up.ps1" Delete "$INSTDIR\auto-update.ps1" - ; CERTBOT CUSTOM END - SetRegView [[ib.py_bitness]] - SetShellVarContext all - IfFileExists "$INSTDIR\${USER_INSTALL_MARKER}" 0 +3 - SetShellVarContext current - Delete "$INSTDIR\${USER_INSTALL_MARKER}" - - Delete $INSTDIR\uninstall.exe + Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\${PRODUCT_ICON}" RMDir /r "$INSTDIR\pkgs" @@ -244,20 +202,3 @@ Function .onMouseOverSection [% endblock mouseover_messages %] FunctionEnd - -Function .onInit - !insertmacro MULTIUSER_INIT -FunctionEnd - -Function un.onInit - !insertmacro MULTIUSER_UNINIT -FunctionEnd - -[% if ib.py_bitness == 64 %] -Function correct_prog_files - ; The multiuser machinery doesn't know about the different Program files - ; folder for 64-bit applications. Override the install dir it set. - StrCmp $MultiUser.InstallMode AllUsers 0 +2 - StrCpy $INSTDIR "$PROGRAMFILES64\${MULTIUSER_INSTALLMODE_INSTDIR}" -FunctionEnd -[% endif %] \ No newline at end of file