{ Copyright 2019-2020 Espressif Systems (Shanghai) CO LTD SPDX-License-Identifier: Apache-2.0 } { SystemCheck states } const SYSTEM_CHECK_STATE_INIT = 0; { No check was executed yet. } SYSTEM_CHECK_STATE_RUNNING = 1; { Check is in progress and can be cancelled. } SYSTEM_CHECK_STATE_COMPLETE = 2; { Check is complete. } SYSTEM_CHECK_STATE_STOPPED = 3; { User stopped the check. } var { RTF View to display content of system check. } SystemCheckViewer: TNewMemo; { Indicate state of System Check. } SystemCheckState:Integer; { Text representation of log messages which are then converte to RTF. } SystemLogText: TStringList; { Message for user which gives a hint how to correct the problem. } SystemCheckHint: String; { Setup Page which displays progress/result of system check. } SystemCheckPage: TOutputMsgWizardPage; { TimeCounter for Spinner animation invoked during command execution. } TimeCounter:Integer; { Spinner is TStringList, because characters like backslash must be escaped and stored on two bytes. } Spinner: TStringList; { Button to request display of full log of system check/installation. } FullLogButton: TNewButton; { Button to request application of available fixtures. } ApplyFixesButton: TNewButton; { Commands which should be executed to fix problems discovered during system check. } Fixes: TStringList; { Button to request Stop of System Checks manually. } StopSystemCheckButton: TNewButton; { Count number of createde virtualenv to avoid collision with previous runs. } VirtualEnvCounter: Integer; { Indicates whether system check was able to find running Windows Defender. } var IsWindowsDefenderEnabled: Boolean; { Const values for user32.dll which allows scrolling of the text view. } const WM_VSCROLL = $0115; SB_BOTTOM = 7; type TMsg = record hwnd: HWND; message: UINT; wParam: Longint; lParam: Longint; time: DWORD; pt: TPoint; end; const PM_REMOVE = 1; { Functions to communicate via Windows API. } function PeekMessage(var lpMsg: TMsg; hWnd: HWND; wMsgFilterMin, wMsgFilterMax, wRemoveMsg: UINT): BOOL; external 'PeekMessageW@user32.dll stdcall'; function TranslateMessage(const lpMsg: TMsg): BOOL; external 'TranslateMessage@user32.dll stdcall'; function DispatchMessage(const lpMsg: TMsg): Longint; external 'DispatchMessageW@user32.dll stdcall'; procedure AppProcessMessage; var Msg: TMsg; begin while PeekMessage(Msg, WizardForm.Handle, 0, 0, PM_REMOVE) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; end; { Render text message for view, add spinner if necessary and scroll the window. } procedure SystemLogRefresh(); begin SystemCheckViewer.Lines := SystemLogText; { Add Spinner to message. } if ((TimeCounter > 0) and (TimeCounter < 6)) then begin SystemCheckViewer.Lines[SystemCheckViewer.Lines.Count - 1] := SystemCheckViewer.Lines[SystemCheckViewer.Lines.Count - 1] + ' [' + Spinner[TimeCounter - 1] + ']'; end; { Scroll window to the bottom of the log - https://stackoverflow.com/questions/64587596/is-it-possible-to-display-the-install-actions-in-a-list-in-inno-setup } SendMessage(SystemCheckViewer.Handle, WM_VSCROLL, SB_BOTTOM, 0); end; { Log message to file and display just a '.' to user so that user is not overloaded by details. } procedure SystemLogProgress(message:String); begin Log(message); if (SystemLogText.Count = 0) then begin SystemLogText.Append(''); end; SystemLogText[SystemLogText.Count - 1] := SystemLogText[SystemLogText.Count - 1] + '.'; SystemLogRefresh(); end; { Log message to file and display it to user as title message with asterisk prefix. } procedure SystemLogTitle(message:String); begin message := '* ' + message; Log(message); SystemLogText.Append(message); SystemLogRefresh(); end; { Log message to file and display it to user. } procedure SystemLog(message:String); begin Log(message); if (SystemLogText.Count = 0) then begin SystemLogText.Append(''); end; SystemLogText[SystemLogText.Count - 1] := SystemLogText[SystemLogText.Count - 1] + message; SystemLogRefresh(); end; { Process timer tick during command execution so that the app keeps communicating with user. } procedure TimerTick(); begin { TimeCounter for animating Spinner. } TimeCounter:=TimeCounter+1; if (TimeCounter = 5) then begin TimeCounter := 1; end; { Redraw Log with Spinner animation. } SystemLogRefresh(); { Give control back to UI so that it can be updated. https://gist.github.com/jakoch/33ac13800c17eddb2dd4 } AppProcessMessage; end; { --- Command line nonblocking exec --- } function NonBlockingExec(command, workdir: String): Integer; var Res: Integer; Handle: Longword; ExitCode: Integer; LogTextAnsi: AnsiString; LogText, LeftOver: String; begin if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin ExitCode := -3; Exit; end; try ExitCode := -1; { SystemLog('Workdir: ' + workdir); } SystemLogProgress(' $ ' + command); Handle := ProcStart(command, workdir) if Handle = 0 then begin SystemLog('[' + CustomMessage('SystemCheckResultError') + ']'); Result := -2; Exit; end; while (ExitCode = -1) and (SystemCheckState <> SYSTEM_CHECK_STATE_STOPPED) do begin ExitCode := ProcGetExitCode(Handle); SetLength(LogTextAnsi, 4096); Res := ProcGetOutput(Handle, LogTextAnsi, 4096) if Res > 0 then begin SetLength(LogTextAnsi, Res); LogText := LeftOver + String(LogTextAnsi); SystemLogProgress(LogText); end; TimerTick(); Sleep(200); end; ProcEnd(Handle); finally if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin Result := -1; end else begin Result := ExitCode; end; end; end; { Execute command for SystemCheck and reset timer so that Spinner will disappear after end of execution. } function SystemCheckExec(command, workdir: String): Integer; begin TimeCounter := 0; Result := NonBlockingExec(command, workdir); TimeCounter := 0; end; { Get formated line from SystemCheck for user. } function GetSystemCheckHint(Command: String; CustomCheckMessageKey:String):String; begin Result := CustomMessage('SystemCheckUnableToExecute') + ' ' + Command + #13#10 + CustomMessage(CustomCheckMessageKey); end; { Add command to list of fixes which can be executed by installer. } procedure AddFix(Command:String); begin { Do not add possible fix command when check command was stopped by user. } if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin Exit; end; Fixes.Append(Command); end; { Execute checks to determine whether Python installation is valid so thet user can choose it to install IDF. } function IsPythonInstallationValid(displayName: String; pythonPath:String): Boolean; var ResultCode: Integer; ScriptFile: String; TempDownloadFile: String; Command: String; VirtualEvnPath: String; VirtualEnvPython: String; RemedyCommand: String; begin SystemLogTitle(CustomMessage('SystemCheckForComponent') + ' ' + displayName + ' '); SystemCheckHint := ''; pythonPath := pythonPath + ' '; Command := pythonPath + '-m pip --version'; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyMissingPip'); Result := False; Exit; end; Command := pythonPath + '-m virtualenv --version'; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyMissingVirtualenv') + #13#10 + pythonPath + '-m pip install --upgrade pip' + #13#10 + pythonPath + '-m pip install virtualenv'; AddFix(pythonPath + '-m pip install --upgrade pip'); AddFix(pythonPath + '-m pip install virtualenv'); Result := False; Exit; end; VirtualEnvCounter := VirtualEnvCounter + 1; VirtualEvnPath := ExpandConstant('{tmp}\') + IntToStr(VirtualEnvCounter) + '-idf-test-venv\'; VirtualEnvPython := VirtualEvnPath + 'Scripts\python.exe '; Command := pythonPath + '-m virtualenv ' + VirtualEvnPath; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyCreateVirtualenv'); Result := False; Exit; end; ScriptFile := ExpandConstant('{tmp}\system_check_virtualenv.py') Command := VirtualEnvPython + ScriptFile + ' ' + VirtualEnvPython; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyPythonInVirtualenv'); Result := False; Exit; end; Command := VirtualEnvPython + '-m pip install --only-binary ":all:" "cryptography>=2.1.4" --no-binary future'; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyBinaryPythonWheel'); Result := False; Exit; end; TempDownloadFile := IntToStr(VirtualEnvCounter) + '-idf-exe-v1.0.1.zip'; ScriptFile := ExpandConstant('{tmp}\system_check_download.py'); Command := VirtualEnvPython + ScriptFile + ExpandConstant(' https://dl.espressif.com/dl/idf-exe-v1.0.1.zip ' + TempDownloadFile); ResultCode := SystemCheckExec(Command , ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyFailedHttpsDownload'); Result := False; Exit; end; if (not FileExists(ExpandConstant('{tmp}\') + TempDownloadFile)) then begin SystemLog(' [' + CustomMessage('SystemCheckResultFail') + '] - ' + CustomMessage('SystemCheckUnableToFindFile') + ' ' + ExpandConstant('{tmp}\') + TempDownloadFile); Result := False; Exit; end; ScriptFile := ExpandConstant('{tmp}\system_check_subprocess.py'); Command := pythonPath + ScriptFile; ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin RemedyCommand := pythonPath + '-m pip uninstall subprocess.run'; SystemCheckHint := GetSystemCheckHint(Command, 'SystemCheckRemedyFailedSubmoduleRun') + #13#10 + RemedyCommand; AddFix(RemedyCommand); Result := False; Exit; end; SystemLog(' [' + CustomMessage('SystemCheckResultOk') + ']'); Result := True; end; procedure FindPythonVersionsFromKey(RootKey: Integer; SubKeyName: String); var CompanyNames: TArrayOfString; CompanyName, CompanySubKey, TagName, TagSubKey: String; ExecutablePath, DisplayName, Version: String; TagNames: TArrayOfString; CompanyId, TagId: Integer; BaseDir: String; begin if not RegGetSubkeyNames(RootKey, SubKeyName, CompanyNames) then begin Log('Nothing found in ' + IntToStr(RootKey) + '\' + SubKeyName); Exit; end; for CompanyId := 0 to GetArrayLength(CompanyNames) - 1 do begin CompanyName := CompanyNames[CompanyId]; if CompanyName = 'PyLauncher' then continue; CompanySubKey := SubKeyName + '\' + CompanyName; Log('In ' + IntToStr(RootKey) + '\' + CompanySubKey); if not RegGetSubkeyNames(RootKey, CompanySubKey, TagNames) then continue; for TagId := 0 to GetArrayLength(TagNames) - 1 do begin TagName := TagNames[TagId]; TagSubKey := CompanySubKey + '\' + TagName; Log('In ' + IntToStr(RootKey) + '\' + TagSubKey); if not GetPythonVersionInfoFromKey(RootKey, SubKeyName, CompanyName, TagName, Version, DisplayName, ExecutablePath, BaseDir) then continue; if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin Exit; end; { Verify Python installation and display hint in case of invalid version or env. } if not IsPythonInstallationValid(DisplayName, ExecutablePath) then begin if ((Length(SystemCheckHint) > 0) and (SystemCheckState <> SYSTEM_CHECK_STATE_STOPPED)) then begin SystemLogTitle(CustomMessage('SystemCheckHint') + ': ' + SystemCheckHint); end; continue; end; PythonVersionAdd(Version, DisplayName, ExecutablePath); end; end; end; procedure FindInstalledPythonVersions(); begin FindPythonVersionsFromKey(HKEY_CURRENT_USER, 'Software\Python'); FindPythonVersionsFromKey(HKEY_LOCAL_MACHINE, 'Software\Python'); FindPythonVersionsFromKey(HKEY_LOCAL_MACHINE, 'Software\Wow6432Node\Python'); end; { Get Boolean for UI to determine whether it make sense to register exceptions to Defender. } function GetWindowsDefenderStatus(): Boolean; var bHasWD: Boolean; szWDPath: String; listPSModulePath: TStringList; ResultCode: Integer; x: Integer; begin Log('Checking PSMODULEPATH for Windows Defender module'); listPSModulePath := TStringList.Create; listPSModulePath.Delimiter := ';'; listPSModulePath.StrictDelimiter := True; listPSModulePath.DelimitedText := GetEnv('PsModulePath'); for x:=0 to (listPSModulePath.Count-1) do begin szWDPath := listPSModulePath[x] + '\Defender' bHasWD := DirExists(szWDPath); if bHasWD then begin break; end end; if not bHasWD then begin Result := False; Exit; end; Log('Checking Windows Services Defender is enabled: (Get-MpComputerStatus).AntivirusEnabled'); ResultCode := SystemCheckExec('powershell -ExecutionPolicy Bypass "if((Get-MpComputerStatus).AntivirusEnabled) { Exit 0 } else { Exit 1 }"', ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin Log('Result code: ' + IntToStr(ResultCode)); Result := False; Exit; end; Result := True; end; { Process user request to stop system checks. } function SystemCheckStopRequest():Boolean; begin { In case of stopped check by user, procees to next/previous step. } if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin Result := True; Exit; end; if (SystemCheckState = SYSTEM_CHECK_STATE_RUNNING) then begin if (MsgBox(CustomMessage('SystemCheckNotCompleteConsent'), mbConfirmation, MB_YESNO) = IDYES) then begin SystemCheckState := SYSTEM_CHECK_STATE_STOPPED; Result := True; Exit; end; end; if (SystemCheckState = SYSTEM_CHECK_STATE_COMPLETE) then begin Result := True; end else begin Result := False; end; end; { Process request to proceed to next page. If the scan is running ask user for confirmation. } function OnSystemCheckValidate(Sender: TWizardPage): Boolean; begin Result := SystemCheckStopRequest(); end; { Process request to go to previous screen (license). Prompt user for confirmation when system check is running. } function OnSystemCheckBackButton(Sender: TWizardPage): Boolean; begin Result := SystemCheckStopRequest(); end; { Process request to stop System Check directly on the screen with System Check by Stop button. } procedure StopSystemCheckButtonClick(Sender: TObject); begin SystemCheckStopRequest(); end; { Check whether site is reachable and that system trust the certificate. } procedure VerifyRootCertificates(); var ResultCode: Integer; Command: String; OutFile: String; begin SystemLogTitle(CustomMessage('SystemCheckRootCertificates') + ' '); { It's necessary to invoke PowerShell *BEFORE* Python. Invoke-Request will retrieve and add Root Certificate if necessary. } { Without the certificate Python is failing to connect to https. } { Windows command to list current certificates: certlm.msc } OutFile := ExpandConstant('{tmp}\check'); Command := 'powershell -ExecutionPolicy Bypass '; Command := Command + 'Invoke-WebRequest -Uri "https://dl.espressif.com/dl/?system_check=win' + GetWindowsVersionString + '" -OutFile "' + OutFile + '-1.txt";'; Command := Command + 'Invoke-WebRequest -Uri "https://github.com/espressif" -OutFile "' + OutFile + '-2.txt";'; {Command := Command + 'Invoke-WebRequest -Uri "https://www.s3.amazonaws.com/" -OutFile "' + OutFile + '-3.txt";';} ResultCode := SystemCheckExec(Command, ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin SystemLog(' [' + CustomMessage('SystemCheckResultWarn') + ']'); SystemLog(CustomMessage('SystemCheckRootCertificateWarning')); end else begin SystemLog(' [' + CustomMessage('SystemCheckResultOk') + ']'); end; end; { Execute system check } procedure ExecuteSystemCheck(); begin { Execute system check only once. Avoid execution in case of back button. } if (SystemCheckState <> SYSTEM_CHECK_STATE_INIT) then begin Exit; end; SystemCheckState := SYSTEM_CHECK_STATE_RUNNING; SystemLogTitle(CustomMessage('SystemCheckStart')); StopSystemCheckButton.Enabled := True; VerifyRootCertificates(); FindInstalledPythonVersions(); if (SystemCheckState <> SYSTEM_CHECK_STATE_STOPPED) then begin SystemLogTitle(CustomMessage('SystemCheckForDefender') + ' '); IsWindowsDefenderEnabled := GetWindowsDefenderStatus(); if (IsWindowsDefenderEnabled) then begin SystemLog(' [' + CustomMessage('SystemCheckResultFound') + ']'); end else begin SystemLog(' [' + CustomMessage('SystemCheckResultNotFound') + ']'); end; end else begin { User cancelled the check, let's enable Defender script so that use can decide to disable it. } IsWindowsDefenderEnabled := True; end; if (SystemCheckState = SYSTEM_CHECK_STATE_STOPPED) then begin SystemLog(''); SystemLogTitle(CustomMessage('SystemCheckStopped')); end else begin SystemLogTitle(CustomMessage('SystemCheckComplete')); SystemCheckState := SYSTEM_CHECK_STATE_COMPLETE; end; { Enable Apply Script button if some fixes are available. } if (Fixes.Count > 0) then begin ApplyFixesButton.Enabled := True; end; StopSystemCheckButton.Enabled := False; end; { Invoke scan of system environment. } procedure OnSystemCheckActivate(Sender: TWizardPage); begin { Display special controls. For some reason the first call of the page does not invoke SystemCheckOnCurPageChanged. } FullLogButton.Visible := True; ApplyFixesButton.Visible := True; StopSystemCheckButton.Visible := True; SystemCheckViewer.Visible := True; ExecuteSystemCheck(); end; { Handle request to display full log from the installation. Open the log in notepad. } procedure FullLogButtonClick(Sender: TObject); var ResultCode: Integer; begin Exec(ExpandConstant('{win}\notepad.exe'), ExpandConstant('{log}'), '', SW_SHOW, ewNoWait, ResultCode); end; { Handle request to apply available fixes. } procedure ApplyFixesButtonClick(Sender: TObject); var ResultCode: Integer; FixIndex: Integer; AreFixesApplied: Boolean; begin if (MsgBox(CustomMessage('SystemCheckApplyFixesConsent'), mbConfirmation, MB_YESNO) = IDNO) then begin Exit; end; ApplyFixesButton.Enabled := false; SystemCheckState := SYSTEM_CHECK_STATE_INIT; SystemLog(''); SystemLogTitle('Starting application of fixes'); AreFixesApplied := True; for FixIndex := 0 to Fixes.Count - 1 do begin ResultCode := SystemCheckExec(Fixes[FixIndex], ExpandConstant('{tmp}')); if (ResultCode <> 0) then begin AreFixesApplied := False; break; end; end; SystemLog(''); if (AreFixesApplied) then begin SystemLogTitle(CustomMessage('SystemCheckFixesSuccessful')); end else begin SystemLogTitle(CustomMessage('SystemCheckFixesFailed')); end; SystemLog(''); Fixes.Clear(); { Restart system check. } ExecuteSystemCheck(); end; { Add Page for System Check so that user is informed about readiness of the system. } procedure CreateSystemCheckPage(); begin { Initialize data structure for Python } InstalledPythonVersions := TStringList.Create(); InstalledPythonDisplayNames := TStringList.Create(); InstalledPythonExecutables := TStringList.Create(); { Create Spinner animation. } Spinner := TStringList.Create(); Spinner.Append('-'); Spinner.Append('\'); Spinner.Append('|'); Spinner.Append('/'); VirtualEnvCounter := 0; Fixes := TStringList.Create(); SystemCheckState := SYSTEM_CHECK_STATE_INIT; SystemCheckPage := CreateOutputMsgPage(wpLicense, CustomMessage('PreInstallationCheckTitle'), CustomMessage('PreInstallationCheckSubtitle'), ''); with SystemCheckPage do begin OnActivate := @OnSystemCheckActivate; OnBackButtonClick := @OnSystemCheckBackButton; OnNextButtonClick := @OnSystemCheckValidate; end; SystemCheckViewer := TNewMemo.Create(WizardForm); with SystemCheckViewer do begin Parent := WizardForm; Left := ScaleX(10); Top := ScaleY(60); ReadOnly := True; Font.Name := 'Courier New'; Height := WizardForm.CancelButton.Top - ScaleY(40); Width := WizardForm.ClientWidth + ScaleX(80); WordWrap := True; Visible := False; end; SystemLogText := TStringList.Create; FullLogButton := TNewButton.Create(WizardForm); with FullLogButton do begin Parent := WizardForm; Left := WizardForm.ClientWidth; Top := SystemCheckViewer.Top + SystemCheckViewer.Height + ScaleY(5); Width := WizardForm.CancelButton.Width; Height := WizardForm.CancelButton.Height; Caption := CustomMessage('SystemCheckFullLogButtonCaption'); OnClick := @FullLogButtonClick; Visible := False; end; ApplyFixesButton := TNewButton.Create(WizardForm); with ApplyFixesButton do begin Parent := WizardForm; Left := WizardForm.ClientWidth - FullLogButton.Width; Top := FullLogButton.Top; Width := WizardForm.CancelButton.Width; Height := WizardForm.CancelButton.Height; Caption := CustomMessage('SystemCheckApplyFixesButtonCaption'); OnClick := @ApplyFixesButtonClick; Visible := False; Enabled := False; end; StopSystemCheckButton := TNewButton.Create(WizardForm); with StopSystemCheckButton do begin Parent := WizardForm; Left := ApplyFixesButton.Left - ApplyFixesButton.Width; Top := FullLogButton.Top; Width := WizardForm.CancelButton.Width; Height := WizardForm.CancelButton.Height; Caption := CustomMessage('SystemCheckStopButtonCaption'); OnClick := @StopSystemCheckButtonClick; Visible := False; Enabled := False; end; { Extract helper files for sanity check of Python environment. } ExtractTemporaryFile('system_check_download.py') ExtractTemporaryFile('system_check_subprocess.py') ExtractTemporaryFile('system_check_virtualenv.py') end; { Process Cancel Button Click event. Prompt user to confirm Cancellation of System check. } { Then continue with normal cancel window. } procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean); begin if ((CurPageId = SystemCheckPage.ID) and (SystemCheckState = SYSTEM_CHECK_STATE_RUNNING)) then begin SystemCheckStopRequest(); end; end; { Display control specific for System Check page. } procedure SystemCheckOnCurPageChanged(CurPageID: Integer); begin FullLogButton.Visible := CurPageID = SystemCheckPage.ID; ApplyFixesButton.Visible := CurPageID = SystemCheckPage.ID; StopSystemCheckButton.Visible := CurPageID = SystemCheckPage.ID; SystemCheckViewer.Visible := CurPageID = SystemCheckPage.ID; end;