From e402993c34ecf342db83bc3f0d8b34ae0d1d127a Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 23 Sep 2019 21:29:18 +0200 Subject: [PATCH] [Windows] Create a certbot renew scheduled task using the installer (#7371) This PR implements the item "register a scheduled task for certificate renewal" from the list of requirements described in #7365. This PR adds required instructions in the NSIS installer for Certbot to create a task, named "Certbot Renew Task" in the Windows Scheduler. This task is run twice a day, to execute the command certbot renew and keep the certificates up-to-date. Uninstalling Certbot will also remove this scheduled task. * Implementation * Corrections * Update template.nsi * Improve scripts * Add a random delay of 12 hours * Synchronize template against default one in pynsist 2.4 * Clean config of scheduled task * Install only in AllUsers mode * Add comments * Remove the logic of single user install --- tools/dev_constraints.txt | 3 + windows-installer/construct.py | 12 +- windows-installer/renew-down.ps1 | 6 + windows-installer/renew-up.ps1 | 15 ++ windows-installer/template.nsi | 257 +++++++++++++++++++++++++++++++ 5 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 windows-installer/renew-down.ps1 create mode 100644 windows-installer/renew-up.ps1 create mode 100644 windows-installer/template.nsi diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index e2bec5e20..c23cf9cce 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -53,6 +53,9 @@ pyasn1==0.1.9 pyasn1-modules==0.0.10 Pygments==2.2.0 pylint==1.9.4 +# If pynsist version is upgraded, our NSIS template windows-installer/template.nsi +# must be upgraded if necessary using the new built-in one from pynsist. +pynsist==2.4 pytest==3.2.5 pytest-cov==2.5.1 pytest-forked==0.2 diff --git a/windows-installer/construct.py b/windows-installer/construct.py index 15296d559..2427c0128 100644 --- a/windows-installer/construct.py +++ b/windows-installer/construct.py @@ -19,7 +19,7 @@ def main(): installer_cfg_path = _generate_pynsist_config(repo_path, build_path) - _prepare_build_tools(venv_path, venv_python) + _prepare_build_tools(venv_path, venv_python, repo_path) _compile_wheels(repo_path, build_path, venv_python) _build_installer(installer_cfg_path, venv_path) @@ -47,12 +47,12 @@ def _compile_wheels(repo_path, build_path, venv_python): subprocess.check_call(command) -def _prepare_build_tools(venv_path, venv_python): +def _prepare_build_tools(venv_path, venv_python, repo_path): print('Prepare build tools') subprocess.check_call([sys.executable, '-m', 'venv', venv_path]) subprocess.check_call(['choco', 'upgrade', '-y', 'nsis']) subprocess.check_call([venv_python, '-m', 'pip', 'install', '--upgrade', 'pip']) - subprocess.check_call([venv_python, '-m', 'pip', 'install', 'wheel', 'pynsist']) + subprocess.check_call([venv_python, os.path.join(repo_path, 'tools', 'pip_install.py'), 'wheel', 'pynsist']) def _copy_assets(build_path, repo_path): @@ -62,6 +62,9 @@ def _copy_assets(build_path, repo_path): os.makedirs(build_path) shutil.copy(os.path.join(repo_path, 'windows-installer', 'certbot.ico'), build_path) shutil.copy(os.path.join(repo_path, 'windows-installer', 'run.bat'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'template.nsi'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-up.ps1'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-down.ps1'), build_path) def _generate_pynsist_config(repo_path, build_path): @@ -83,6 +86,7 @@ target=$INSTDIR\\run.bat [Build] directory=nsis +nsi_template=template.nsi installer_name=certbot-{certbot_version}-installer-{installer_suffix}.exe [Python] @@ -92,6 +96,8 @@ bitness={python_bitness} [Include] local_wheels=wheels\\*.whl files=run.bat + renew-up.ps1 + renew-down.ps1 [Command certbot] entry_point=certbot.main:main diff --git a/windows-installer/renew-down.ps1 b/windows-installer/renew-down.ps1 new file mode 100644 index 000000000..60dc4d9e6 --- /dev/null +++ b/windows-installer/renew-down.ps1 @@ -0,0 +1,6 @@ +$taskName = "Certbot Renew Task" + +$exists = Get-ScheduledTask | Where-Object {$_.TaskName -like $taskName} +if ($exists) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false +} diff --git a/windows-installer/renew-up.ps1 b/windows-installer/renew-up.ps1 new file mode 100644 index 000000000..c6a5fd9ea --- /dev/null +++ b/windows-installer/renew-up.ps1 @@ -0,0 +1,15 @@ +function Get-ScriptDirectory { Split-Path $MyInvocation.ScriptName } +$down = Join-Path (Get-ScriptDirectory) 'renew-down.ps1' +& $down + +$taskName = "Certbot Renew Task" + +$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -Command "certbot renew"' +$delay = New-TimeSpan -Hours 12 +$triggerAM = New-ScheduledTaskTrigger -Daily -At 12am -RandomDelay $delay +$triggerPM = New-ScheduledTaskTrigger -Daily -At 12pm -RandomDelay $delay +# NB: For now scheduled task is set up under SYSTEM account because Certbot Installer installs Certbot for all users. +# If in the future we allow the Installer to install Certbot for one specific user, the scheduled task will need to +# switch to this user, since Certbot will be available only for him. +$principal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest +Register-ScheduledTask -Action $action -Trigger $triggerAM,$triggerPM -TaskName $taskName -Description "Execute twice a day the 'certbot renew' command, to renew managed certificates if needed." -Principal $principal diff --git a/windows-installer/template.nsi b/windows-installer/template.nsi new file mode 100644 index 000000000..0f366c22a --- /dev/null +++ b/windows-installer/template.nsi @@ -0,0 +1,257 @@ +; 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. +; Original file can be found here: https://github.com/takluyver/pynsist/blob/2.4/nsist/pyapp.nsi + +!define PRODUCT_NAME "[[ib.appname]]" +!define PRODUCT_VERSION "[[ib.version]]" +!define PY_VERSION "[[ib.py_version]]" +!define PY_MAJOR_VERSION "[[ib.py_major_version]]" +!define BITNESS "[[ib.py_bitness]]" +!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" +!define MUI_ABORTWARNING +!define MUI_ICON "[[icon]]" +!define MUI_UNICON "[[icon]]" + +; UI pages +[% block ui_pages %] +!insertmacro MUI_PAGE_WELCOME +[% 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 +[% endblock ui_pages %] +!insertmacro MUI_LANGUAGE "English" +[% endblock modernui %] + +Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" +OutFile "${INSTALLER_NAME}" +ShowInstDetails show + +Section -SETTINGS + SetOutPath "$INSTDIR" + SetOverwrite ifnewer +SectionEnd + +[% block sections %] + +Section "!${PRODUCT_NAME}" sec_app + SetRegView [[ib.py_bitness]] + 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 %] + SetOutPath "[[destination]]" + [% for file in group %] + File "[[ file ]]" + [% endfor %] + [% endfor %] + + ; Install directories + [% for dir, destination in ib.install_dirs %] + SetOutPath "[[ pjoin(destination, dir) ]]" + File /r "[[dir]]\*.*" + [% endfor %] + [% endblock install_files %] + + [% block install_shortcuts %] + ; Install shortcuts + ; The output path becomes the working directory for shortcuts + SetOutPath "%HOMEDRIVE%\%HOMEPATH%" + [% if single_shortcut %] + [% for scname, sc in ib.shortcuts.items() %] + CreateShortCut "$SMPROGRAMS\[[scname]].lnk" "[[sc['target'] ]]" \ + '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]" + [% endfor %] + [% else %] + [# Multiple shortcuts: create a directory for them #] + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + [% for scname, sc in ib.shortcuts.items() %] + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\[[scname]].lnk" "[[sc['target'] ]]" \ + '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]" + [% endfor %] + [% endif %] + SetOutPath "$INSTDIR" + [% endblock install_shortcuts %] + + [% block install_commands %] + [% if has_commands %] + 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: + [% endif %] + [% endblock install_commands %] + + ; Byte-compile Python files. + DetailPrint "Byte-compiling Python modules..." + nsExec::ExecToLog '[[ python ]] -m compileall -q "$INSTDIR\pkgs"' + WriteUninstaller $INSTDIR\uninstall.exe + ; Add ourselves to Add/remove programs + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "DisplayName" "${PRODUCT_NAME}" + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "InstallLocation" "$INSTDIR" + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "DisplayIcon" "$INSTDIR\${PRODUCT_ICON}" + [% if ib.publisher is not none %] + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "Publisher" "[[ib.publisher]]" + [% endif %] + WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "NoModify" 1 + WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ + "NoRepair" 1 + + ; CERTBOT CUSTOM BEGIN + ; Execute ps script to create the certbot renew task + DetailPrint "Setting up certbot renew scheduled task" + nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\renew-up.ps1"' + ; CERTBOT CUSTOM END + + ; Check if we need to reboot + IfRebootFlag 0 noreboot + MessageBox MB_YESNO "A reboot is required to finish the installation. Do you wish to reboot now?" \ + /SD IDNO IDNO noreboot + Reboot + noreboot: +SectionEnd + +Section "Uninstall" + ; CERTBOT CUSTOM BEGIN + ; Execute ps script to remove the certbot renew task + nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\renew-down.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\${PRODUCT_ICON}" + RMDir /r "$INSTDIR\pkgs" + + ; Remove ourselves from %PATH% + [% block uninstall_commands %] + [% if has_commands %] + nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" remove "$INSTDIR\bin"' + [% endif %] + [% endblock uninstall_commands %] + + [% block uninstall_files %] + ; Uninstall files + [% for file, destination in ib.install_files %] + Delete "[[pjoin(destination, file)]]" + [% endfor %] + ; Uninstall directories + [% for dir, destination in ib.install_dirs %] + RMDir /r "[[pjoin(destination, dir)]]" + [% endfor %] + [% endblock uninstall_files %] + + [% block uninstall_shortcuts %] + ; Uninstall shortcuts + [% if single_shortcut %] + [% for scname in ib.shortcuts %] + Delete "$SMPROGRAMS\[[scname]].lnk" + [% endfor %] + [% else %] + RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}" + [% endif %] + [% endblock uninstall_shortcuts %] + RMDir $INSTDIR + DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" +SectionEnd + +[% endblock sections %] + +; Functions + +Function .onMouseOverSection + ; Find which section the mouse is over, and set the corresponding description. + FindWindow $R0 "#32770" "" $HWNDPARENT + GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI) + + [% block mouseover_messages %] + StrCmp $0 ${sec_app} "" +2 + SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}" + + [% 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