spiegel-keyman/windows/src/desktop/setup/RunTools.pas
Marc Durdin 3415724966 fix(windows): setup now allows choice of source
Where more than one source of a file is available, e.g. a local
keymandesktop.msi which is older than the current published version
online, then Setup will choose the newer, online version by default,
but allows the user to change the install source in the Options dialog
to have an offline setup if they prefer.

This means that the 'best' location is now chosen at startup, and then
the user can override that.

Relates to #3689.
2020-10-29 11:18:25 +11:00

894 lines
31 KiB
ObjectPascal

(*
Name: RunTools
Copyright: Copyright (C) 2003-2017 SIL International.
Documentation:
Description:
Create Date: 30 Dec 2010
Modified Date: 24 Jun 2014
Authors: mcdurdin
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
History: 30 Dec 2010 - mcdurdin - I2562 - Have a common install utils unit after splitting Keyman Desktop and OEM setup dialogs
31 Dec 2010 - mcdurdin - I2605 - Fixup crash starting Desktop when downloaded locale is selected post-upgrade
14 Jan 2011 - mcdurdin - I2645 - Refactor install process steps - move SetupMSI to individual forms
17 Jan 2011 - mcdurdin - I1917 - Fix references to patchurl, patchsize; show correct file size and save to correct filename
21 Feb 2011 - mcdurdin - I2740 - Auto Update should close down Keyman before starting the update
22 Feb 2011 - mcdurdin - I2747 - After upgrading from KM7, the backup registry key is not deleted
22 Feb 2011 - mcdurdin - I2314 - setup.log saved to incorrect location
22 Feb 2011 - mcdurdin - I2651 - Install does not set desired default options
22 Feb 2011 - mcdurdin - I2738 - Auto Update does not start automatically
22 Feb 2011 - mcdurdin - I2741 - After install tutorial starts automatically
22 Feb 2011 - mcdurdin - I2748 - Delete backup path from shell user registry, not admin user registry
22 Feb 2011 - mcdurdin - I2749 - Audit all uses of TRegistry, TTntRegistry and replace with TShellUserRegistry where appropriate
22 Feb 2011 - mcdurdin - I2755 - Setup.log should be stored in Diag folder
22 Feb 2011 - mcdurdin - I2754 - Auto update is too silent and reboots unexpectedly for user
22 Feb 2011 - mcdurdin - I2757 - Installer does not wait for kmshell actions to complete before continuing
22 Feb 2011 - mcdurdin - I2756 - Keyman Desktop is not starting after update
28 Feb 2011 - mcdurdin - I2768 - Keyman Desktop setup fails on a new computer (regression)
18 Mar 2011 - mcdurdin - I2792 - Fix crash logging import of older keyboards
03 May 2011 - mcdurdin - I2896 - Check root certificate availability
04 Jul 2011 - mcdurdin - I2970 - Keyman installer should not set defaults for OEM products
18 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support
18 May 2012 - mcdurdin - I3326 - V9.0 - Add missing MsiEnumRelatedProducts from jwamsi
08 Jun 2012 - mcdurdin - I3349 - V9.0 - Consolidate all process creation into TUtilExecute
26 Jun 2012 - mcdurdin - I3379 - KM9 - Remove old Winapi references now in Delphi libraries
17 Aug 2012 - mcdurdin - I3377 - KM9 - Update code references from 8.0 to 9.0
19 Oct 2012 - mcdurdin - I3476 - V9.0 - Fixup additional hints and warnings around string conversion
15 Jun 2012 - mcdurdin - I3355 - Keyman Developer (and Desktop) sometimes reboot automatically with auto upgrade
03 Nov 2012 - mcdurdin - I3500 - V9.0 - Merge of I3355 - Keyman Developer (and Desktop) sometimes reboot automatically with auto upgrade
24 Jun 2014 - mcdurdin - I4293 - V9.0 - Setup bootstrapper does not check for V8 upgrade
*)
unit RunTools; // I3306
interface
uses
System.Classes,
System.SysUtils,
Vcl.Controls,
Winapi.Messages,
Winapi.Windows,
Keyman.Setup.System.InstallInfo,
UfrmDownloadProgress;
type
TStatusEvent = procedure(const Status: WideString) of object;
TRunTools = class
private
FSilent: Boolean;
FPromptForReboot: Boolean; // I3355 // I3500
FErrorLog: TFileStream;
StatusMax: Integer;
FOnStatus: TStatusEvent;
FOnline: Boolean;
FRunUpgrade6: Boolean;
FRunUpgrade7: Boolean;
FRunUpgrade8: Boolean; // I4293
FRunUpgrade9: Boolean;
FRunUpgrade10: Boolean;
constructor Create;
function IsNewerVersionInstalled(const NewVersion: WideString): Boolean;
procedure Status(Text: WideString);
function CacheMSIFile(msiLocation: TInstallInfoFileLocation): WideString;
procedure FinishCacheMSIFile(msiLocation: TInstallInfoFileLocation;
InstallSuccess: Boolean);
function InstallMSI(msiLocation: TInstallInfoFileLocation): Boolean;
procedure ConfigFirstRun(StartKeyman,StartWithWindows,
CheckForUpdates,StartDisabled,StartWithConfiguration,
AutomaticallyReportUsage: Boolean);
procedure PrepareForReboot(res: Cardinal);
function RestartWindows: Boolean;
procedure RunVersion6Upgrade(const kmshellpath: WideString);
procedure RunVersion7Upgrade(const KMShellPath: WideString);
procedure RunVersion8Upgrade(const KMShellPath: WideString); // I4293
procedure RunVersion9Upgrade(const KMShellPath: WideString);
procedure RunVersion10Upgrade(const KMShellPath: WideString);
procedure CloseKeymanApplications; // I2740
procedure DeleteBackupPath; // I2747
procedure WaitFor(hProcess: THandle; var Waiting, Cancelled: Boolean); // I3349
procedure WriteToLog(const msg: string);
procedure RunVersion11To13Upgrade(const KMShellPath: WideString);
public
destructor Destroy; override;
procedure CheckInternetConnectedState;
function DoInstall(Handle: THandle;
StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled,
StartWithConfiguration, AutomaticallyReportUsage: Boolean): Boolean;
procedure LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean = True);
procedure LogInfo(const msg: string; ShowDialogIfNotSilent: Boolean = False);
class procedure CheckInstalledVersion(msiLocation: TInstallInfoFileLocation);
property Silent: Boolean read FSilent write FSilent;
property PromptForReboot: Boolean read FPromptForReboot write FPromptForReboot; // I3355 // I3500
property Online: Boolean read FOnline;
property OnStatus: TStatusEvent read FOnStatus write FOnStatus;
property RunUpgrade6: Boolean read FRunUpgrade6 write FRunUpgrade6;
property RunUpgrade7: Boolean read FRunUpgrade7 write FRunUpgrade7;
property RunUpgrade8: Boolean read FRunUpgrade8 write FRunUpgrade8; // I4293
property RunUpgrade9: Boolean read FRunUpgrade9 write FRunUpgrade9;
property RunUpgrade10: Boolean read FRunUpgrade10 write FRunUpgrade10;
end;
function GetRunTools: TRunTools;
procedure CheckMSIResult(msiLocation: TInstallInfoFileLocation; res: UINT);
implementation
uses
System.Variants,
System.Win.ComObj,
Vcl.Forms,
Winapi.psapi,
Winapi.ShlObj,
Winapi.Tlhelp32,
jwamsi,
jwawintype,
bootstrapmain,
GetOsVersion,
Keyman.Setup.System.ResourceDownloader,
Keyman.Setup.System.SetupUILanguageManager,
Keyman.System.UpgradeRegistryKeys,
KeymanPaths,
KeymanVersion,
OnlineConstants,
RegistryHelpers,
RegistryKeys,
SetupStrings,
SFX,
TntDialogHelp,
ErrorControlledRegistry,
utilsystem,
utilexecute,
VersionInfo;
var
FRunTools: TRunTools = nil;
const
UpgradeCode_Keyman11Plus = '{c70af17c-8b9e-47a1-a099-b65aee3dc8b4}'; // Keyman 11+
ProductCode_Keyman7_1Light = '{35E06B45-17C0-406C-B94F-70EFF1EC9278}'; // Keyman 7.1 Light
ProductCode_Keyman7_1Pro = '{04C8710E-3D29-4A25-80A2-A56853A4267D}'; // Keyman 7.1 Pro
ProductCode_Keyman8 = '{18E9B728-8E4E-48DF-9E9F-6F3086A1FE04}'; // Keyman 8.0
ProductCode_Keyman9 = '{E6806190-7B09-4A8C-8C95-25982589D919}'; // Keyman 9.0
ProductCode_Keyman10 = '{A20AFB02-7581-4019-9229-5312308FBA1E}'; // Keyman 10.0
function GetRunTools: TRunTools;
begin
if not Assigned(FRunTools) then
FRunTools := TRunTools.Create;
Result := FRunTools;
end;
procedure CheckMSIResult(msiLocation: TInstallInfoFileLocation; res: UINT);
begin
if res <> ERROR_SUCCESS then
if Assigned(msiLocation) then
raise Exception.Create('Failed to install '+msiLocation.Path+': ['+IntToStr(res)+'] '+SysErrorMessage(res))
else
raise Exception.Create('Failed to install msi: ['+IntToStr(res)+'] '+SysErrorMessage(res));
end;
{ TRunTools }
constructor TRunTools.Create;
begin
end;
destructor TRunTools.Destroy;
begin
FreeAndNil(FErrorLog);
inherited;
end;
function TRunTools.DoInstall(Handle: THandle;
StartAfterInstall, StartWithWindows, CheckForUpdates, StartDisabled,
StartWithConfiguration, AutomaticallyReportUsage: Boolean): Boolean;
var
msiLocation: TInstallInfoFileLocation;
begin
if FInstallInfo.ShouldInstallKeyman
then StatusMax := 6 + FInstallInfo.Packages.Count
else StatusMax := FInstallInfo.Packages.Count;
msiLocation := FInstallInfo.MsiInstallLocation;
if Assigned(msiLocation) and FInstallInfo.ShouldInstallKeyman then
begin
if msiLocation.LocationType = iilOnline then
begin
LogInfo('Downloading '+msiLocation.Url);
if not TResourceDownloader.Execute(FInstallInfo, msiLocation, FSilent) then
begin
LogInfo('Failed to download '+msiLocation.Url);
Exit(False);
end;
end;
Status(FInstallInfo.Text(ssStatusInstalling));
CloseKeymanApplications; // I2740
if not InstallMSI(msiLocation) then
Exit(False);
end;
ConfigFirstRun(StartAfterInstall,StartWithWindows,CheckForUpdates,
StartDisabled,StartWithConfiguration,AutomaticallyReportUsage);
Result := True;
end;
procedure TRunTools.LogError(const msg: WideString; ShowDialogIfNotSilent: Boolean = True);
begin
WriteToLog('ERROR: '+msg);
if not FSilent and ShowDialogIfNotSilent then
ShowMessageW(msg);
end;
procedure TRunTools.WriteToLog(const msg: string);
const
nl: WideString = #13#10;
var
path: WideString;
begin
if not Assigned(FErrorLog) then
begin
path := TKeymanPaths.ErrorLogPath + 'setup.log'; // I2314
if System.SysUtils.FileExists(path) then
begin
FErrorLog := TFileStream.Create(path, fmOpenReadWrite);
FErrorLog.Seek(0, soFromEnd);
end
else
FErrorLog := TFileStream.Create(path, fmCreate);
end;
FErrorLog.Write(PWideChar(msg+nl)^, Length(msg+nl)*2);
end;
procedure TRunTools.LogInfo(const msg: string; ShowDialogIfNotSilent: Boolean = False);
begin
WriteToLog('INFO: '+msg);
if not FSilent and ShowDialogIfNotSilent then
ShowMessageW(msg);
end;
function TRunTools.IsNewerVersionInstalled(const NewVersion: WideString): Boolean;
begin
Result := (FInstallInfo.InstalledVersion.Version <> '') and
(CompareVersions(FInstallInfo.InstalledVersion.Version, NewVersion) <= 0);
end;
function CloseThreadWindowProc(hwnd: THandle; lParam: LPARAM): BOOL; stdcall; // I2740
begin
PostMessage(hwnd, WM_CLOSE, 0, 0);
Result := True;
end;
procedure TRunTools.CloseKeymanApplications; // I2740
const
KeymanApplicationNames: array[0..2] of string = ('kmshell.exe', 'keyman.exe', 'tsysinfo.exe');
var
i: Integer;
cbNeeded, cb: DWORD;
processes: PDWORD;
nProcesses: Cardinal;
BaseName: array[0..260] of char;
hModule, hProcess: THandle;
n: Integer;
FAppName: string;
te: tagTHREADENTRY32;
FProcessIDs: array of Cardinal;
pid: PDWORD;
var
hSnapshot: THandle;
begin
cb := 1000 * sizeof(DWORD);
processes := AllocMem(cb);
repeat
if not EnumProcesses(processes, cb, cbNeeded) then Exit;
if cb = cbNeeded then
begin
cb := cb * 2;
ReallocMem(processes, cb);
cbNeeded := cb;
end;
until cb > cbNeeded;
try
SetLength(FProcessIDs, 0);
pid := processes;
nProcesses := cbNeeded div sizeof(DWORD);
for i := 0 to nProcesses - 1 do
begin
hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, pid^);
if hProcess <> 0 then
try
if EnumProcessModules(hProcess, @hModule, sizeof(hModule), cbNeeded) then
begin
if GetModuleBaseName(hProcess, hModule, baseName, Sizeof(BaseName) div SizeOf(BaseName[0])) > 0 then
begin
FAppName := ExtractFileName(BaseName);
for n := 0 to High(KeymanApplicationNames) do
if SameText(FAppName, KeymanApplicationNames[n]) then
begin
SetLength(FProcessIDs, Length(FProcessIDs)+1);
FProcessIDs[High(FProcessIDs)] := pid^;
end;
end;
end;
finally
CloseHandle(hProcess);
end;
Inc(pid);
end;
finally
FreeMem(processes);
end;
hSnapshot := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,0);
if hSnapshot <> INVALID_HANDLE_VALUE then
try
FillChar(te,sizeof(te),0);
te.dwSize := SizeOf(te);
if Thread32First(hSnapshot, te) then
begin
repeat
for i := 0 to High(FProcessIDs) do
if te.th32OwnerProcessID = FProcessIDs[i] then
begin
EnumThreadWindows(te.th32ThreadID, @CloseThreadWindowProc, 0);
end;
until not Thread32Next(hSnapshot, te);
end;
finally
CloseHandle(hSnapshot);
end;
end;
procedure TRunTools.Status(Text: WideString);
begin
if Assigned(FOnStatus) then
FOnStatus(Text);
end;
procedure TRunTools.CheckInternetConnectedState;
type
TInternetGetConnectedState = function(lpdwFlags: LPDWORD; dwReserved: DWORD): BOOL; stdcall;
var
hModule: THandle;
FInternetGetConnectedState: TInternetGetConnectedState;
flags: DWord;
begin
FOnline := False;
hModule := LoadLibrary('wininet.dll');
if hModule <> 0 then
begin
FInternetGetConnectedState := GetProcAddress(hModule, 'InternetGetConnectedState');
if Assigned(FInternetGetConnectedState) then
begin
FOnline := FInternetGetConnectedState(@flags, 0);
end;
FreeLibrary(hModule);
end;
end;
function TRunTools.CacheMSIFile(msiLocation: TInstallInfoFileLocation): WideString;
var
path: WideString;
begin
path := GetFolderPath(CSIDL_PROGRAM_FILES_COMMON) + SFolder_CachedInstallerFiles+'\'+msiLocation.ProductCode; // I2561
if not System.SysUtils.ForceDirectories(path) then
raise Exception.Create('Failed to cache installer MSI file (code 1,'+IntToStr(GetLastError)+'): '+SysErrorMessage(GetLastError));
Result := path+'\'+ExtractFileName(msiLocation.Path);
if FileExists(PWideChar(Result)) then
begin
// Backup the existing cached installer until after the upgrade
RenameFile(Result, Result + '.1');
end;
if not CopyFile(PWideChar(msiLocation.Path), PWideChar(Result), False) then
raise Exception.Create('Failed to cache installer MSI file (code 2,'+IntToStr(GetLastError)+'): '+SysErrorMessage(GetLastError));
end;
procedure TRunTools.FinishCacheMSIFile(msiLocation: TInstallInfoFileLocation; InstallSuccess: Boolean);
var
newmsi, path: WideString;
begin
path := GetFolderPath(CSIDL_PROGRAM_FILES_COMMON) + SFolder_CachedInstallerFiles+'\'+msiLocation.ProductCode; // I2561
newmsi := path+'\'+ExtractFileName(msiLocation.Path);
if System.SysUtils.FileExists(newmsi+'.1') then
begin
if InstallSuccess then
System.SysUtils.DeleteFile(newmsi+'.1')
else
begin
System.SysUtils.DeleteFile(newmsi);
RenameFile(newmsi+'.1', newmsi);
end;
end
else if not InstallSuccess then
begin
System.SysUtils.DeleteFile(newmsi);
RemoveDir(path);
RemoveDir(ExtractFileDir(path)); // may fail if other products cached there, we don't care
RemoveDir(ExtractFileDir(ExtractFileDir(path))); // may fail if other products installed there, we don't care
end;
end;
{$EXTERNALSYM MsiEnumRelatedProducts}
const
msilib = 'msi.dll';
{$IFDEF UNICODE}
AWSuffix = 'W';
{$ELSE}
AWSuffix = 'A';
{$ENDIF UNICODE}
var
_MsiEnumRelatedProducts: Pointer;
{$WARNINGS OFF}
function MsiEnumRelatedProducts(lpUpgradeCode: LPCTSTR; dwReserved: DWORD;
iProductIndex: DWORD; lpProductBuf: LPTSTR): UINT; stdcall; // I3326
begin
GetProcedureAddress(_MsiEnumRelatedProducts, msilib, 'MsiEnumRelatedProducts' + AWSuffix);
asm
MOV ESP, EBP
POP EBP
JMP [_MsiEnumRelatedProducts]
end;
end;
{$WARNINGS ON}
// TODO: move to another unit so we can mock?
class procedure TRunTools.CheckInstalledVersion(msiLocation: TInstallInfoFileLocation);
var
hProduct: MSIHANDLE;
buf: array[0..64] of WideChar;
sz: DWord;
UpgradeCode: WideString;
i: DWord;
ver: TMSIInfo;
begin
if not Assigned(msiLocation) or (msiLocation.LocationType = iilOnline) then
begin
UpgradeCode := UpgradeCode_Keyman11Plus
end
else
begin
CheckMSIResult(msiLocation, MsiOpenPackageExW(PWideChar(msiLocation.Path), MSIOPENPACKAGEFLAGS_IGNOREMACHINESTATE, hProduct)); // I3476
try
sz := 64;
CheckMSIResult(msiLocation, MsiGetProductPropertyW(hProduct, 'UpgradeCode', buf, @sz));
UpgradeCode := buf;
sz := 64;
CheckMSIResult(msiLocation, MsiGetProductPropertyW(hProduct, 'ProductCode', buf, @sz));
msiLocation.ProductCode := buf;
finally
MsiCloseHandle(hProduct);
end;
end;
i := 0;
if MsiEnumRelatedProducts(PWideChar(UpgradeCode), 0, i, buf) = ERROR_SUCCESS then
begin
ver.ProductCode := buf;
sz := 64;
CheckMSIResult(msiLocation, MsiGetProductInfoW(PWideChar(ver.ProductCode), INSTALLPROPERTY_VERSIONSTRING, buf, @sz));
ver.Version := buf;
FInstallInfo.InstalledVersion := ver;
end;
end;
function TRunTools.InstallMSI(msiLocation: TInstallInfoFileLocation): Boolean;
var
pcode: array[0..39] of Char;
res: Cardinal;
ReinstallMode: WideString;
FCacheFileName: WideString;
FLogFileName: WideString;
begin
Result := True;
if not IsNewerVersionInstalled(msiLocation.Version) then // I2560
begin
ReinstallMode := 'REBOOTPROMPT=S REBOOT=ReallySuppress'; // I2754 - Auto update is too silent
if (FInstallInfo.InstalledVersion.Version <> '') and (FInstallInfo.InstalledVersion.ProductCode = msiLocation.ProductCode) then
begin
ReinstallMode := ReinstallMode + ' REINSTALLMODE=vomus REINSTALL=ALL';
end
else
begin
// Remove older versions of Keyman now. We'll still get the upgrade desired
// because we've backed up the relevant keys for reapplication post-install.
// Version 11 and later do not need this treatment as they are upgraded in-place
// with the file and registry locations remaining static
if (MsiGetProductCode(ProductCode_Keyman7_1Light, pcode) = ERROR_SUCCESS) or // Keyman 7.1 Light
(MsiGetProductCode(ProductCode_Keyman7_1Pro, pcode) = ERROR_SUCCESS) or // Keyman 7.1 Pro
(MsiGetProductCode(ProductCode_Keyman8, pcode) = ERROR_SUCCESS) or // Keyman 8.0
(MsiGetProductCode(ProductCode_Keyman9, pcode) = ERROR_SUCCESS) or // Keyman 9.0
(MsiGetProductCode(ProductCode_Keyman10, pcode) = ERROR_SUCCESS) then // Keyman 10.0
begin
Status(FInstallInfo.Text(ssStatusRemovingOlderVersions));
MsiConfigureProduct(pcode, INSTALLLEVEL_DEFAULT, INSTALLSTATE_ABSENT);
// We'll ignore errors ...
end;
end;
{ Log the install to the diag folder }
FLogFileName := TKeymanPaths.ErrorLogPath(ChangeFileExt(ExtractFileName(msiLocation.Path), '')); // I1610 // I2755 // I2792
//ForceDirectories(GetErrLogPath); // I2768
MsiEnableLogW(INSTALLLOGMODE_VERBOSE, PWideChar(FLogFileName), 0);
{ Cache the msi file to avoid source dependencies for repair, patch, uninstall, bleagh }
FCacheFileName := CacheMSIFile(msiLocation); // I3476
Status(FInstallInfo.Text(ssStatusInstalling));
res := MsiInstallProductW(PWideChar(FCacheFileName), PWideChar(ReinstallMode));
FinishCacheMSIFile(msiLocation, // I3476
(res = ERROR_SUCCESS_REBOOT_REQUIRED) or (res = ERROR_SUCCESS_REBOOT_INITIATED) or (res = ERROR_SUCCESS));
case res of
ERROR_SUCCESS: ;
ERROR_SUCCESS_REBOOT_REQUIRED, ERROR_SUCCESS_REBOOT_INITIATED:
begin
PrepareForReboot(res); // We need to continue this install after reboot
Result := False;
Exit;
end;
ERROR_INSTALL_USEREXIT: begin Result := False; Exit; end;
else
CheckMSIResult(msiLocation, res);
end;
end;
if not FSilent then
MsiSetInternalUI(INSTALLUILEVEL_FULL, nil);
end;
procedure TRunTools.WaitFor(hProcess: THandle; var Waiting, Cancelled: Boolean); // I3349
begin
case MsgWaitForMultipleObjects(1, hProcess, FALSE, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0 + 1:
Application.ProcessMessages;
WAIT_OBJECT_0, WAIT_ABANDONED_0:
Waiting := False;
end;
end;
procedure TRunTools.ConfigFirstRun(StartKeyman,StartWithWindows,CheckForUpdates,
StartDisabled,StartWithConfiguration,AutomaticallyReportUsage: Boolean);
var
i: Integer;
s: WideString;
FKMShellPath: WideString;
FExitCode: Cardinal;
msiLocation: TInstallInfoFileLocation;
FKMShellVersion: string;
procedure DoInstallPackages;
var
s: string;
pack: TInstallInfoPackage;
packLocation: TInstallInfoPackageFileLocation;
FExitCode: Cardinal;
begin
s := '-nowelcome -s -i ';
for pack in FInstallInfo.Packages do
begin
if pack.ShouldInstall then
begin
packLocation := pack.InstallLocation;
if Assigned(packLocation) then
begin
if packLocation.LocationType = iilOnline then
begin
LogInfo('Downloading '+packLocation.Url);
if not TResourceDownloader.Execute(FInstallInfo, packLocation, FSilent) then
begin
LogInfo('Failed to download '+packLocation.Url);
pack.ShouldInstall := False;
Continue;
end;
end;
// Need to check kmshell version >= 14.0 for support for non-default
// language registration into local machine context; will not install
// current user context.
//
// If kmshell.exe is an older version, then the default language for
// the keyboard will be registered *and* installed, and furthermore,
// the user's language selection will be ignored (sadface).
// Note, if the BCP47 code specified here is in the list of the package's
// supported BCP47 codes, then it would have been registered anyway. This
// allows for custom BCP47 codes to be registered while elevated.
if CompareVersions(SKeymanVersion_Min_SpecifyLanguage, FKMShellVersion) >= 0
then s := s + '"'+packLocation.Path+'='+pack.BCP47+'" '
else s := s + '"'+packLocation.Path+'" '; // I3476
end;
end;
end;
TUtilExecute.WaitForProcess('"'+FKMShellPath+'" '+s, ExtractFilePath(FKMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
if CompareVersions(SKeymanVersion_Min_SpecifyLanguage, FKMShellVersion) >= 0 then
begin
// For each package add the BCP47 install details for current user.
// This relies on the TIP having been registered by the previous invocation
// of kmshell.exe -i
s := '-s -install-tips-for-packages ';
for pack in FInstallInfo.Packages do
begin
if pack.ShouldInstall then
s := s + '"'+pack.ID+'='+pack.BCP47+'" ';
end;
TUtilExecute.CreateProcessAsShellUser(FKMShellPath, '"'+FKMShellPath+'" '+s, True, FExitCode);
end;
end;
begin
FKMShellPath := TKeymanPaths.KeymanDesktopInstallPath(TKeymanPaths.S_KMShell);
if System.SysUtils.FileExists(FKMShellPath) then
begin
RunVersion6Upgrade(FKMShellPath);
RunVersion7Upgrade(FKMShellPath); // I2548
RunVersion8Upgrade(FKMShellPath); // I4293
RunVersion9Upgrade(FKMShellPath);
RunVersion10Upgrade(FKMShellPath);
RunVersion11To13Upgrade(FKMShellPath);
FKMShellVersion := GetFileVersionString(FKMShellPath);
// Install packages for all users; keyboard languages (TIPs) are only
// installed for the current user
if FInstallInfo.Packages.Count > 0 then
DoInstallPackages;
// Configure Keyman Desktop for initial install, both local-machine,
// and current-user
s := '-firstrun=';
if StartWithWindows then s := s + 'StartWithWindows,';
if CheckForUpdates then s := s + 'CheckForUpdates,';
if AutomaticallyReportUsage then s := s + 'AutomaticallyReportUsage,';
msiLocation := FInstallInfo.MsiInstallLocation;
if Assigned(msiLocation) and (
(FInstallInfo.InstalledVersion.Version = '') or (FInstallInfo.InstalledVersion.ProductCode <> msiLocation.ProductCode)
) then
begin
s := s + 'InstallDefaults,'; // I2651
s := s + ' -defaultuilanguage='+TSetupUILanguageManager.ActiveLocale;
end;
if StartDisabled then
begin
s := s + ' -disablePackages=';
for i := 0 to FInstallInfo.Packages.Count - 1 do
begin
if i > 0 then s := s + ',';
s := s + '"'+FInstallInfo.Packages[i].ID+'"';
end;
end;
TUtilExecute.WaitForProcess('"'+FKMShellPath+'" '+s, ExtractFilePath(FKMShellPath), SW_SHOWNORMAL, WaitFor); // I2605 - for admin-installed packages // I3349
FExitCode := 0;
if not TUtilExecute.CreateProcessAsShellUser(FKMShellPath, '"'+FKMShellPath+'" '+s, True, FExitCode) then // I2757
LogError('Failed to setup default options for Keyman Desktop: '+SysErrorMessage(GetLastError))
else if FExitCode = 2 then
begin
LogError('Failed to setup Keyman Desktop to start with Windows; this action may have been blocked by security software.');
end;
end;
DeleteBackupPath; // I2747
if StartKeyman then // I2738
begin
if System.SysUtils.FileExists(FKMShellPath) then
begin
s := '"'+FKMShellPath+'"';
if StartWithConfiguration then
s := s + ' -startWithConfiguration';
if not TUtilExecute.CreateProcessAsShellUser(FKMShellPath, s, False) then // I2741
LogError('Failed to start Keyman Desktop: '+SysErrorMessage(GetLastError), False); // I2756
end;
end;
Status(FInstallInfo.Text(ssStatusComplete));
end;
procedure TRunTools.PrepareForReboot(res: Cardinal);
begin
with CreateHKCURegistry do // I2749
try
if OpenKey(SRegKey_WindowsRunOnce_CU, True) then
WriteString(SRegValue_WindowsRunOnce_Setup, '"'+ParamStr(0)+'" -c');
finally
Free;
end;
if res = ERROR_SUCCESS_REBOOT_REQUIRED then
begin
if GetRunTools.PromptForReboot and
(MessageDlgW(FInstallInfo.Text(ssQueryRestart), mtConfirmation, [mbYes,mbNo], 0) = mrYes) then
begin
if not RestartWindows then
GetRunTools.LogError(FInstallInfo.Text(ssErrorUnableToAutomaticallyRestart));
end
else
GetRunTools.LogError(FInstallInfo.Text(ssMustRestart));
end;
end;
type
PTOKEN_PRIVILEGES = ^TOKEN_PRIVILEGES;
{$EXTERNALSYM PTOKEN_PRIVILEGES}
_TOKEN_PRIVILEGES = record
PrivilegeCount: DWORD;
Privileges: array [0..0] of LUID_AND_ATTRIBUTES;
end;
TAdjustTokenPrivileges = function (TokenHandle: THandle; DisableAllPrivileges: BOOL;
const NewState: TTokenPrivileges; BufferLength: DWORD;
PreviousState: PTokenPrivileges; var ReturnLength: DWORD): BOOL; stdcall;
TOpenProcessToken = function (ProcessHandle: THandle; DesiredAccess: DWORD;
var TokenHandle: THandle): BOOL; stdcall;
TLookupPrivilegeValue = function (lpSystemName, lpName: PWideChar;
var lpLuid: TLargeInteger): BOOL; stdcall;
function TRunTools.RestartWindows: Boolean;
var
hToken: THandle;
tkp: TOKEN_PRIVILEGES;
FAdjustTokenPrivileges: TAdjustTokenPrivileges;
FOpenProcessToken: TOpenProcessToken;
FLookupPrivilegeValue: TLookupPrivilegeValue;
hAdvApi32: Cardinal;
retlen: Cardinal;
const
SE_SHUTDOWN_NAME: WideString = 'SeShutdownPrivilege';
SHTDN_REASON_MAJOR_APPLICATION = $00040000;
SHTDN_REASON_MINOR_INSTALLATION = $00000002;
SHTDN_REASON_FLAG_PLANNED = DWORD($80000000);
begin
Result := False;
hAdvApi32 := LoadLibrary('advapi32.dll');
if hAdvApi32 = 0 then Exit;
FOpenProcessToken := GetProcAddress(hAdvApi32, 'OpenProcessToken');
if not Assigned(FOpenProcessToken) then Exit;
FLookupPrivilegeValue := GetProcAddress(hAdvApi32, 'LookupPrivilegeValueW');
if not Assigned(FLookupPrivilegeValue) then Exit;
FAdjustTokenPrivileges := GetProcAddress(hAdvApi32, 'AdjustTokenPrivileges');
if not Assigned(FAdjustTokenPrivileges) then Exit;
// Get a token for this process.
if not FOpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken) then Exit;
// Get the LUID for the shutdown privilege.
if not FLookupPrivilegeValue(nil, PWideChar(SE_SHUTDOWN_NAME), tkp.Privileges[0].Luid) then Exit;
tkp.PrivilegeCount := 1; // one privilege to set
tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
// Get the shutdown privilege for this process.
FAdjustTokenPrivileges(hToken, False, tkp, 0, nil, retlen);
if GetLastError() <> ERROR_SUCCESS then Exit;
Result := ExitWindowsEx(EWX_REBOOT, SHTDN_REASON_MAJOR_APPLICATION or SHTDN_REASON_MINOR_INSTALLATION or SHTDN_REASON_FLAG_PLANNED);
end;
procedure TRunTools.RunVersion6Upgrade(const kmshellpath: WideString);
var
s: WideString;
begin
if FRunUpgrade6 then
begin
s := '"'+KMShellPath+'" -upgradekeyboards='; // I2548
TUtilExecute.WaitForProcess(s+'6,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
TUtilExecute.WaitForProcess(s+'6,user', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349 // I4293
end;
end;
procedure TRunTools.RunVersion7Upgrade(const KMShellPath: WideString);
var
s: WideString;
begin
if FRunUpgrade7 then
begin
s := '"'+KMShellPath+'" -upgradekeyboards='; // I2548
TUtilExecute.WaitForProcess(s+'7,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
TUtilExecute.WaitForProcess(s+'7,user', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349 // I4293
end;
end;
procedure TRunTools.RunVersion8Upgrade(const KMShellPath: WideString); // I4293
var
s: WideString;
begin
if FRunUpgrade8 then
begin
s := '"'+KMShellPath+'" -upgradekeyboards='; // I2548
TUtilExecute.WaitForProcess(s+'8,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
TUtilExecute.WaitForProcess(s+'8,user', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
end;
end;
procedure TRunTools.RunVersion9Upgrade(const KMShellPath: WideString);
var
s: WideString;
begin
if FRunUpgrade9 then
begin
s := '"'+KMShellPath+'" -upgradekeyboards='; // I2548
TUtilExecute.WaitForProcess(s+'9,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
end;
end;
procedure TRunTools.RunVersion10Upgrade(const KMShellPath: WideString);
var
s: WideString;
begin
if FRunUpgrade10 then
begin
s := '"'+KMShellPath+'" -upgradekeyboards='; // I2548
TUtilExecute.WaitForProcess(s+'10,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
end;
end;
procedure TRunTools.RunVersion11To13Upgrade(const KMShellPath: WideString);
begin
// We run this after installation; it is idempotent and will upgrade all keyboards
// from earlier versions to version 14+
TUtilExecute.WaitForProcess('"'+KMShellPath+'" -upgradekeyboards=13,admin', ExtractFilePath(KMShellPath), SW_SHOWNORMAL, WaitFor); // I3349
end;
procedure TRunTools.DeleteBackupPath; // I2747
begin
with CreateHKLMRegistry do // I2749
try
if KeyExists(SRegKey_UpgradeBackupPath_LM) then DeleteKey(SRegKey_UpgradeBackupPath_LM);
finally
Free;
end;
with CreateHKCURegistry do // I2748, I2749
try
if KeyExists(SRegKey_UpgradeBackupPath_CU) then DeleteKey(SRegKey_UpgradeBackupPath_CU);
finally
Free;
end;
end;
initialization
finalization
FreeAndNil(FRunTools);
end.