spiegel-keyman/windows/src/desktop/setup/bootstrapmain.pas
2023-01-25 16:09:14 +07:00

668 lines
22 KiB
ObjectPascal

(*
Name: bootstrapmain
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Create Date: 4 Jun 2007
Modified Date: 23 Oct 2014
Authors: mcdurdin
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
History: 04 Jun 2007 - mcdurdin - Initial version
05 Jun 2007 - mcdurdin - I817 - Fix -x option
19 Jun 2007 - mcdurdin - I817 - Translate to Unicode and remove Forms dependence
14 Sep 2007 - mcdurdin - I1065 - -s silent option in setup.exe stills asks about updated version
14 Sep 2007 - mcdurdin - I1066 - -s option does not do a fully silent install in setup.exe
14 Sep 2007 - mcdurdin - I1063 - setup.exe crashing when an update is found online rather than installing it
27 Mar 2008 - mcdurdin - I1306 - Fix -s option not doing fully silent extract
14 Jun 2008 - mcdurdin - Close handles after starting app
28 Jul 2008 - mcdurdin - I1158 - Report error codes
30 Dec 2010 - mcdurdin - I2562 - EULA in bootstrapper
31 Dec 2010 - mcdurdin - I2615 - Fix hidden controls when Alt pressed in Vista
31 Dec 2010 - mcdurdin - I2611 - Incorrect icon for setup form
21 Feb 2011 - mcdurdin - I2738 - Auto update does not start automatically when triggered
31 Mar 2011 - mcdurdin - I2847 - Version 8.0 silent upgrade runs keyboard import from version 7.0 and shouldn't
03 Oct 2011 - mcdurdin - I3081 - Create target folder when -x specified
04 Nov 2011 - mcdurdin - I3126 - Add flag to control msi UI level
18 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support
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
02 Feb 2012 - mcdurdin - I2975 - VistaAltFixUnit can crash on shutdown
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
28 Feb 2014 - mcdurdin - I4099 - V9.0 - Keyman Setup dialog is still 8.0 style
24 Jun 2014 - mcdurdin - I4293 - V9.0 - Setup bootstrapper does not check for V8 upgrade
12 Aug 2014 - mcdurdin - I4365 - V9.0 - Installer still doesn't enforce Win7 or later
16 Oct 2014 - mcdurdin - I4460 - V9.0 - Setup bootstrapper should handle upgrade scenarios with a prompt
23 Oct 2014 - mcdurdin - I4470 - Internet Explorer 9.0 of later required [CrashID:kmshell.exe_9.0.472.0_script_TfrmMain_0]
*)
unit bootstrapmain; // I3306
interface
uses
System.Classes,
System.Generics.Collections,
System.SysUtils,
Keyman.Setup.System.InstallInfo,
SetupStrings;
var
FInstallInfo: TInstallInfo = nil;
procedure Run;
function FormatFileSize(Size: Integer): string;
implementation
uses
System.StrUtils,
System.TypInfo,
Vcl.Forms,
Winapi.ActiveX,
Winapi.Windows,
CommonControls,
ErrorControlledRegistry,
GetOsVersion,
Keyman.Setup.System.OnlineResourceCheck,
Keyman.Setup.System.SetupUILanguageManager,
Keyman.System.UpgradeRegistryKeys,
KeymanPaths,
KeymanVersion,
RegistryHelpers,
RegistryKeys,
RunTools,
SetupForm,
SFX,
TntDialogHelp,
UfrmDownloadProgress,
UfrmRunDesktop,
Upload_Settings,
utilexecute;
type
TSetupBootstrap = class
private
ProgramPath: string;
FExtractOnly: Boolean;
FContinueSetupFilename: string;
FStartAfterInstall: Boolean; // I2738
FDisableUpgradeFrom6Or7Or8: Boolean; // I2847 // I4293
FPromptForReboot: Boolean; // I3355 // I3500
FSilent: Boolean;
FForceOffline: Boolean;
FInstallDefaults: Boolean;
FTier, FPackages, FExtractOnly_Path: string;
function CheckForOldVersionScenario: Boolean;
procedure InstallKeyboardsInOldVersion(const ShellPath: string);
procedure DoExtractOnly(FSilent: Boolean; const FExtractOnly_Path: string);
function CreateTempDir: string;
procedure RemoveTempDir(const path: string);
procedure ProcessCommandLine(
var FPromptForReboot, FSilent, FForceOffline, FExtractOnly: Boolean;
var FContinueSetupFilename: string;
var FStartAfterInstall, FDisableUpgradeFrom6Or7Or8, FInstallDefaults: Boolean;
var FPackages, FExtractPath, FTier: string);
procedure SetExitVal(c: Integer);
function IsKeymanDesktop7Installed: string;
function IsKeymanDesktop8Installed: string;
function GetResourcesFromOnline(FSilent: Boolean;
var FForceOffline: Boolean): Boolean;
procedure LocateResourcesCallback(Owner: TfrmDownloadProgress;
var Result: Boolean);
procedure DeletePath(const path: WideString);
public
procedure Run;
end;
var
FNiceExitCodes: Boolean = True; // always, now
const
ICC_PROGRESS_CLASS = $00000020; // progress
procedure Run;
var
SetupBootstrap: TSetupBootstrap;
begin
SetupBootstrap := TSetupBootstrap.Create;
try
SetupBootstrap.Run;
finally
SetupBootstrap.Free;
end;
end;
procedure TSetupBootstrap.Run;
var
FTempPath: string;
frmDownloadProgress: TfrmDownloadProgress;
msiLocation: TInstallInfoFileLocation;
FResult: Boolean;
BEGIN
CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
try
try
Vcl.Forms.Application.Icon.LoadFromResourceID(hInstance, 1); // I2611
InitCommonControl(ICC_PROGRESS_CLASS);
TSetupUILanguageManager.SetDefault;
FTempPath := CreateTempDir;
try
FInstallInfo := TInstallInfo.Create(FTempPath);
try
{ Display the dialog }
ProcessCommandLine(
FPromptForReboot, FSilent, FForceOffline, FExtractOnly,
FContinueSetupFilename, FStartAfterInstall,
FDisableUpgradeFrom6Or7Or8, FInstallDefaults, FPackages, FExtractOnly_Path, FTier); // I2738, I2847 // I3355 // I3500 // I4293
GetRunTools.Silent := FSilent;
if FContinueSetupFilename <> '' then
begin
if not FileExists(FContinueSetupFilename) then
begin
GetRunTools.LogError('Control file '+FContinueSetupFilename+' for continuing setup is missing. Cannot continue.');
SetExitVal(ERROR_FILE_NOT_FOUND);
Exit;
end;
FInstallInfo.LoadFromJSONFile(FContinueSetupFilename);
// We're using an existing temp folder, so throw away the one we
// just created and use the other one instead.
RemoveDir(FTempPath);
FTempPath := FInstallInfo.TempPath;
end
else if FExtractOnly then
begin
DoExtractOnly(FSilent, FExtractOnly_Path);
Exit;
end;
// There are two possible paths for files: ProgramPath and TempPath,
// and also files that can be downloaded during the installation
// (into TempPath).
ProgramPath := ExtractFilePath(ParamStr(0));
if FContinueSetupFilename = '' then
begin
// We only need to collect install info if we are not
// continuing a previous setup run.
if not FSilent then
begin
frmDownloadProgress := TfrmDownloadProgress.Create(nil);
try
frmDownloadProgress.Callback := LocateResourcesCallback;
if frmDownloadProgress.ShowModal = mrCancel then
Exit;
finally
frmDownloadProgress.Free;
end;
end
else
begin
FResult := True;
LocateResourcesCallback(nil, FResult);
if not FResult then
Exit;
end;
end;
with TfrmRunDesktop.Create(nil) do // I2562
try
ContinueSetup := FContinueSetupFilename <> '';
StartAfterInstall := FStartAfterInstall; // I2738
DisableUpgradeFrom6Or7Or8 := FDisableUpgradeFrom6Or7Or8; // I2847 // I4293
InstallDefaults := FInstallDefaults;
if FSilent
then DoInstall(True, FPromptForReboot) // I3355 // I3500
else ShowModal;
finally
Free;
end;
finally
FreeAndNil(FInstallInfo);
end;
SetExitVal(ERROR_SUCCESS);
finally
if FContinueSetupFilename <> '' then
begin
System.SysUtils.DeleteFile(FContinueSetupFilename);
end;
if not GetRunTools.MustReboot then
begin
// If we are going to reboot, then we don't want to delete the
// temporary files, because they will be used to continue setup
// after rebooting.
RemoveTempDir(FTempPath);
end;
end;
except
on e:Exception do
begin
// For the future, we may consider logging exceptions with Sentry.
// However, Sentry adds a 400kb library to the bootstrap which we
// would need to embed and extract at startup in order to do this
// capture. That's a bit of a downer for this project, which we are
// trying (somewhat unsuccessfully) to keep as lightweight as we can.
GetRunTools.LogError('Exception '+e.ClassName+': '+e.Message);
end;
end;
finally
CoUninitialize;
end;
end;
procedure TSetupBootstrap.LocateResourcesCallback(Owner: TfrmDownloadProgress; var Result: Boolean);
var
FProcessFileName: string;
begin
Result := False;
if Assigned(Owner) then
begin
Owner.Caption := FInstallInfo.Text(ssApplicationTitle);
if not Owner.Status(FInstallInfo.Text(ssBootstrapExtractingBundle), 0, 4) then Exit;
end;
// Extract SFX archive into temp path
ProcessArchive(FInstallInfo.TempPath);
// Get the list of anticipated packages from:
// 1. the program filename
// 2. parameters
// 3. files extracted from the archive
// 4. files in the same folder as this program
// The filename of this executable can be changed to tell it which
// packages to download, e.g. keyman-setup.khmer_angkor.km.exe tells
// it to download khmer_angkor from the Keyman cloud and install it
// for bcp47 tag km. See the setup documentation for more
// examples.
{$IFDEF DEBUG}
// In order to run this in the debugger, it's much easier to define
// an environment variable than to rename the executable, but we don't
// want this except for debug builds.
FProcessFileName := GetEnvironmentVariable('KEYMAN_SETUP_TESTFILENAME');
if FProcessFileName = '' then
{$ENDIF}
FProcessFileName := ParamStr(0);
FInstallInfo.LocatePackagesAndTierFromFilename(FProcessFileName);
// Additionally, packages can be specified on the command line, with
// the -p parameter, e.g. -p khmer_angkor=km,sil_euro_latin=fr
FInstallInfo.LocatePackagesFromParameter(FPackages);
// Lookup a tier from command line parameter
if FTier <> '' then
FInstallInfo.Tier := FTier;
// This loads setup.inf, if present, for various additional strings and settings
// The bundled installer usually contains a setup.inf.
if FileExists(FInstallInfo.TempPath + 'setup.inf') then
FInstallInfo.LoadSetupInf(FInstallInfo.TempPath)
else if FileExists(ProgramPath + 'setup.inf') then
FInstallInfo.LoadSetupInf(ProgramPath);
if Assigned(Owner) then
if not Owner.Status(FInstallInfo.Text(ssBootstrapCheckingPackages), 1) then Exit;
// Finally, load details for any .kmp packages referenced in setup.inf
FInstallInfo.LoadLocalPackagesMetadata;
if Assigned(Owner) and not FForceOffline then
if not Owner.Status(FInstallInfo.Text(ssBootstrapCheckingForUpdates), 2) then Exit;
// Try and get information from online
if not GetResourcesFromOnline(FSilent, FForceOffline) then
begin
SetExitVal(ERROR_FILE_NOT_FOUND);
Exit;
end;
if Assigned(Owner) then
if not Owner.Status(FInstallInfo.Text(ssBootstrapCheckingInstalledVersions), 3) then Exit;
FInstallInfo.CheckPackageLocations;
// If the user is trying to install on downlevel version of Windows (Vista or earlier),
// we can simplify their life by installing the packages into an existing Keyman
// install, or point them to the old version of Keyman otherwise.
if CheckForOldVersionScenario then // I4460
Exit;
TRunTools.CheckInstalledVersion(FInstallInfo.MsiInstallLocation);
// Looks for installation state for Keyman, and determines best packages for installation.
// If no .msi can be found, and Keyman is not installed, this will show/log an error and
// abort installation.
if not FInstallInfo.CheckMsiUpgradeScenarios then
begin
// TODO: this should be refactored together with the retry strategy for online check above
// TODO: Delineate between log messages and dialogs.
// TODO: localize
GetRunTools.LogError('A valid Keyman install could not be found offline. Please connect to the Internet or allow this installer through your firewall in order to install Keyman.');
SetExitVal(ERROR_NO_MORE_FILES);
Exit;
end;
// Default scenario is that if any newer installer is available, then
// Setup should install it.
FInstallInfo.ShouldInstallKeyman := FInstallInfo.IsNewerAvailable;
if Assigned(Owner) then
if not Owner.Status(FInstallInfo.Text(ssBootstrapReady), 4) then Exit;
Result := True;
end;
function TSetupBootstrap.GetResourcesFromOnline(FSilent: Boolean; var FForceOffline: Boolean): Boolean;
begin
if FForceOffline then
Exit(True);
repeat
try
GetRunTools.CheckInternetConnectedState;
if GetRunTools.Online then
TOnlineResourceCheck.QueryServer(FSilent, FInstallInfo);
// We've succeeded.
Exit(True);
except
on E:Exception do
begin
GetRunTools.LogInfo('Could not connect to site: '+E.Message);
if FSilent then
begin
// We log and attempt to continue
FForceOffline := True;
end
else
begin
case MessageDlgW(
Trim(FInstallInfo.Text(ssOffline)+#13#10#13#10+FInstallInfo.Text(ssOffline2)+#13#10#13#10+FInstallInfo.Text(ssOffline3)),
mtError, mbAbortRetryIgnore, 0) of
mrAbort: Exit(False);
mrRetry: Continue;
mrIgnore: FForceOffline := True;
end;
end;
end;
end;
until FForceOffline;
Result := True;
end;
function TSetupBootstrap.CheckForOldVersionScenario: Boolean; // I4460
var
OldKMShellPath: string;
begin
if not (GetOS in [osLegacy, osVista]) then // I4365
Exit(False);
if FInstallInfo.Packages.Count = 0 then
begin
// No keyboards installed, so we determine install based on Windows version
if MessageDlgW(FInstallInfo.Text(ssOldOsVersionDownload), mtConfirmation, mbOkCancel, 0) = mrOk then
TUtilExecute.URL(MakeKeymanURL(URLPath_ArchivedDownloads));
SetExitVal(ERROR_OLD_WIN_VERSION);
Exit(True);
end;
OldKMShellPath := IsKeymanDesktop8Installed;
if OldKMShellPath = '' then OldKMShellPath := IsKeymanDesktop7Installed;
if OldKMShellPath <> '' then
begin
if MessageDlgW(FInstallInfo.Text(ssOldOsVersionInstallKeyboards), mtConfirmation, mbYesNoCancel, 0) = mrYes then
InstallKeyboardsInOldVersion(OldKMShellPath);
end
else
begin
if MessageDlgW(FInstallInfo.Text(ssOldOsVersionDownload), mtConfirmation, mbOkCancel, 0) = mrOk then
TUtilExecute.URL(MakeKeymanURL(URLPath_ArchivedDownloads));
end;
SetExitVal(ERROR_OLD_WIN_VERSION);
Exit(True);
end;
// TODO: move this into a separate unit: it deals with installing keyboards into
// an existing Keyman install on downlevel versions of Windows (e.g. Vista). So
// we are constrained in how we do this -- no ability to do language associations
// etc.
procedure TSetupBootstrap.InstallKeyboardsInOldVersion(const ShellPath: string); // I4460
procedure DoInstall(pack: TInstallInfoPackage; const silentFlag: string);
var
location: TInstallInfoPackageFileLocation;
begin
location := pack.InstallLocation;
if Assigned(location) and pack.ShouldInstall then
begin
if location.LocationType = iilOnline then
Assert(FALSE, 'TODO: implement download of this resource');
//TODO:
TUtilExecute.WaitForProcess('"' + ShellPath + '" '+silentFlag+' -i "'+location.Path+'"', FInstallInfo.TempPath);
end;
end;
var
pack: TInstallInfoPackage;
begin
if FInstallInfo.Packages.Count = 1 then
begin
DoInstall(FInstallInfo.Packages[0], '');
end
else
begin
for pack in FInstallInfo.Packages do
DoInstall(pack, '-s');
ShowMessageW('All packages have been installed');
end;
end;
procedure TSetupBootstrap.DoExtractOnly(FSilent: Boolean; const FExtractOnly_Path: string);
var
path: string;
begin
path := FExtractOnly_Path; // I3476
if path = '' then
path := '.';
if (path <> '.') and (path <> '.\') and not DirectoryExists(path) then // I3081 // I3476
begin
if not CreateDir(path) then // I3476
begin
GetRunTools.LogError('Setup could not create the target folder '+path); // I3476
SetExitVal(Integer(GetLastError));
Exit;
end;
end;
if not ProcessArchive(path) then
begin
GetRunTools.LogError('This file was not a valid self-extracting archive. The files should already be in the same folder as the archive.');
SetExitVal(ERROR_BAD_FORMAT);
Exit;
end;
GetRunTools.LogInfo('All files extracted from the archive to '+path+'\.', True); // I3476
SetExitVal(ERROR_SUCCESS);
end;
function TSetupBootstrap.CreateTempDir: string;
var
buf: array[0..260] of WideChar;
path: string;
begin
GetTempPath(MAX_PATH-1, buf);
path := ExcludeTrailingPathDelimiter(buf); // I3476
GetTempFileName(PWideChar(path), 'kmt', 0, buf); // I3476
path := buf; // I3476
if FileExists(buf) then DeleteFile(buf); // I3476
// NOTE: race condition here...
CreateDirectory(buf, nil); // I3476
Result := IncludeTrailingPathDelimiter(path);
end;
procedure TSetupBootstrap.DeletePath(const path: WideString);
var
fd: TWin32FindDataW;
n: DWord;
begin
n := FindFirstFile(PWideChar(path + '\*.*'), fd);
if n = INVALID_HANDLE_VALUE then Exit;
repeat
DeleteFile(PWideChar(path + '\' + fd.cFileName));
until not FindNextFile(n, fd);
FindClose(n);
RemoveDirectory(PWideChar(path));
end;
procedure TSetupBootstrap.RemoveTempDir(const path: string);
begin
if path <> '' then
DeletePath(ExcludeTrailingPathDelimiter(path)); // I3476
end;
procedure TSetupBootstrap.ProcessCommandLine(
var FPromptForReboot, FSilent, FForceOffline, FExtractOnly: Boolean;
var FContinueSetupFilename: string;
var FStartAfterInstall, FDisableUpgradeFrom6Or7Or8, FInstallDefaults: Boolean;
var FPackages, FExtractPath, FTier: string
);
var
i: Integer;
begin
FPromptForReboot := True; // I3355 // I3500
FSilent := False;
FForceOffline := False;
FExtractOnly := False;
FContinueSetupFilename := '';
FDisableUpgradeFrom6Or7Or8 := False; // I2847 // I4293
FStartAfterInstall := True; // I2738
FInstallDefaults := False;
i := 1;
while i <= ParamCount do
begin
if WideSameText(ParamStr(i), '-c') then
begin
Inc(i);
FContinueSetupFilename := ParamStr(i);
end
else if WideSameText(ParamStr(i), '-s') then
begin
FSilent := True;
FStartAfterInstall := False;
FPromptForReboot := False; // I3355 // I3500
end
else if WideSameText(ParamStr(i), '-au') then // I2738
begin
// auto update - options for more flexibility later...
FSilent := True;
FForceOffline := True;
FStartAfterInstall := True;
FDisableUpgradeFrom6Or7Or8 := True; // I2847 // I4293
end
else if WideSameText(ParamStr(i), '-d') then
FInstallDefaults := True
else if WideSameText(ParamStr(i), '-o') then
FForceOffline := True
else if WideSameText(ParamStr(i), '-r') then
// previously, 'nice exit codes'
else if WideSameText(ParamStr(i), '-x') then
begin
Inc(i);
FExtractOnly := True;
FExtractPath := ParamStr(i);
if FExtractPath = '' then
FExtractPath := ExtractFilePath(ParamStr(0));
end
else if SameText(ParamStr(i), '-p') then
begin
// e.g. -p khmer_angkor=km,sil_euro_latin=fr
Inc(i);
FPackages := ParamStr(i);
end
else if SameText(ParamStr(i), '-t') then
begin
Inc(i);
FTier := ParamStr(i).ToLower.Trim;
if not FTier.Equals(TIER_ALPHA) and not FTier.Equals(TIER_BETA) and not FTier.Equals(TIER_STABLE) then
begin
FTier := '';
end;
end;
Inc(i);
end;
end;
procedure TSetupBootstrap.SetExitVal(c: Integer);
begin
if c = ERROR_SUCCESS
then GetRunTools.LogInfo('Install finished successfully')
else GetRunTools.LogError('Install failed, exiting with error code '+IntToStr(c), False);
ExitCode := c;
end;
function TSetupBootstrap.IsKeymanDesktop7Installed: string; // I4460
begin
with CreateHKLMRegistry do
try
if OpenKeyReadOnly(SRegKey_Keyman70_LM) and ValueExists('root path') //TODO -- test this
then Result := IncludeTrailingPathDelimiter(ReadString('root path')) + TKeymanPaths.S_KMShell
else Exit('');
if not FileExists(Result) then
Result := '';
finally
Free;
end;
end;
function TSetupBootstrap.IsKeymanDesktop8Installed: string; // I4460
begin
with CreateHKLMRegistry do
try
if OpenKeyReadOnly(SRegKey_Keyman80_LM) and ValueExists('root path') //TODO -- test this
then Result := IncludeTrailingPathDelimiter(ReadString('root path')) + TKeymanPaths.S_KMShell
else Exit('');
if not FileExists(Result) then
Result := '';
finally
Free;
end;
end;
function FormatFileSize(Size: Integer): string;
begin
if Size > 1024*1024 then
Result := Format('%.1fMB', [Size/1024/1024])
else if Size > 1024 then
Result := Format('%dKB',[Size div 1024])
else if Size = 1 then
Result := '1 byte'
else
Result := Format('%d bytes', [Size]);
end;
end.