unit Uninstall;

{
  Inno Setup
  Copyright (C) 1997-2008 Jordan Russell
  Portions by Martijn Laan
  For conditions of distribution and use, see LICENSE.TXT.

  Uninstaller

  $jrsoftware: issrc/Projects/Uninstall.pas,v 1.85 2010/01/21 11:14:26 mlaan Exp $

  NOTE: This unit was derived from Uninst.dpr rev 1.38.
}

interface

procedure RunUninstaller;
procedure HandleUninstallerEndSession;

implementation

uses
  Windows, SysUtils, Messages, Forms, PathFunc, CmnFunc, CmnFunc2, Undo, Msgs,
  MsgIDs, InstFunc, Struct, SetupEnt, UninstProgressForm, UninstSharedFileForm,
  FileClass, ScriptRunner, DebugClient, SetupTypes, Logging, Main, Helper,
  SpawnServer;

type
  TExtUninstallLog = class(TUninstallLog)
  private
    FLastUpdateTime: DWORD;
    FNoSharedFileDlgs: Boolean;
    FRemoveSharedFiles: Boolean;
  protected
    procedure HandleException; override;
    function ShouldRemoveSharedFile(const Filename: String): Boolean; override;
    procedure StatusUpdate(StartingCount, CurCount: Integer); override;
  end;

const
  WM_KillFirstPhase = WM_USER + 333;

var
  UninstallExitCode: DWORD = 1;
  UninstExeFile, UninstDataFile, UninstMsgFile: String;
  UninstLogFile: TFile;
  UninstLog: TExtUninstallLog = nil;
  Title: String;
  DidRespawn, SecondPhase: Boolean;
  EnableLogging, Silent, VerySilent, NoRestart: Boolean;
  LogFilename: String;
  InitialProcessWnd, FirstPhaseWnd, DebugWnd: HWND;
  OldWindowProc: Pointer;
{$IFNDEF UNICODE}
  UninstLeadBytes: set of Char;
{$ENDIF}

procedure ShowExceptionMsg;
var
  Msg: String;
begin
  if ExceptObject is EAbort then
    Exit;
  Msg := GetExceptMessage;
  Log('Exception message:');
  LoggedAppMessageBox(PChar(Msg), Pointer(SetupMessages[msgErrorTitle]),
    MB_OK or MB_ICONSTOP, True, IDOK);
    { ^ use a Pointer cast instead of a PChar cast so that it will use "nil"
      if SetupMessages[msgErrorTitle] is empty due to the messages not being
      loaded yet. MessageBox displays 'Error' as the caption if the lpCaption
      parameter is nil. }
end;

procedure TExtUninstallLog.HandleException;
begin
  ShowExceptionMsg;
end;

function TExtUninstallLog.ShouldRemoveSharedFile(const Filename: String): Boolean;
const
  SToAll: array[Boolean] of String = ('', ' to All');
begin
  if Silent or VerySilent then
    Result := True
  else begin
    if not FNoSharedFileDlgs then begin
      { FNoSharedFileDlgs will be set to True if a "...to All" button is clicked }
      FRemoveSharedFiles := ExecuteRemoveSharedFileDlg(Filename,
        FNoSharedFileDlgs);
      LogFmt('Remove shared file %s? User chose %s%s', [Filename, SYesNo[FRemoveSharedFiles], SToAll[FNoSharedFileDlgs]]);
    end;
    Result := FRemoveSharedFiles;
  end;
end;

procedure InitializeUninstallProgressForm;
begin
  UninstallProgressForm := TUninstallProgressForm.Create(nil);
  UninstallProgressForm.Initialize(Title, UninstLog.AppName);
  if CodeRunner <> nil then begin
    try
      CodeRunner.RunProcedure('InitializeUninstallProgressForm', [''], False);
    except
      Log('InitializeUninstallProgressForm raised an exception (fatal).');
      raise;
    end;
  end;
  if not VerySilent then begin
    UninstallProgressForm.Show;
    { Ensure the form is fully painted now in case
      CurUninstallStepChanged(usUninstall) take a long time to return }
    UninstallProgressForm.Update;
  end;
end;

procedure TExtUninstallLog.StatusUpdate(StartingCount, CurCount: Integer);
var
  NowTime: DWORD;
begin
  { Only update the progress bar if it's at the beginning or end, or if
    30 ms has passed since the last update (so that updating the progress
    bar doesn't slow down the actual uninstallation process). }
  NowTime := GetTickCount;
  if (Cardinal(NowTime - FLastUpdateTime) >= Cardinal(30)) or
     (StartingCount = CurCount) or (CurCount = 0) then begin
    FLastUpdateTime := NowTime;
    UninstallProgressForm.UpdateProgress(StartingCount - CurCount, StartingCount);
  end;
  Application.ProcessMessages;
end;

function LoggedMessageBoxFmt1(const ID: TSetupMessageID; const Arg1: String;
  const Title: String; const Flags: UINT; const Suppressible: Boolean;
  const Default: Integer): Integer;
begin
  Result := LoggedAppMessageBox(PChar(FmtSetupMessage1(ID, Arg1)), PChar(Title),
    Flags, Suppressible, Default);
end;

procedure RaiseLastError(const S: String);
var
  ErrorCode: DWORD;
begin
  ErrorCode := GetLastError;
  raise Exception.Create(FmtSetupMessage(msgLastErrorMessage,
    [S, IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
end;

function Exec(const Filename: String; const Parms: String): THandle;
var
  CmdLine: String;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
begin
  CmdLine := '"' + Filename + '" ' + Parms;

  FillChar(StartupInfo, SizeOf(StartupInfo), 0);
  StartupInfo.cb := SizeOf(StartupInfo);
  if not CreateProcess(nil, PChar(CmdLine), nil, nil, False, 0, nil, nil,
     StartupInfo, ProcessInfo) then
    RaiseLastError(SetupMessages[msgLdrCannotExecTemp]);
  CloseHandle(ProcessInfo.hThread);
  Result := ProcessInfo.hProcess;
end;

function ProcessMsgs: Boolean;
var
  Msg: TMsg;
begin
  Result := False;
  while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do begin
    if Msg.Message = WM_QUIT then begin
      Result := True;
      Break;
    end;
    TranslateMessage(Msg);
    DispatchMessage(Msg);
  end;
end;

function FirstPhaseWindowProc(Wnd: HWND; Msg: UINT; wParam: WPARAM;
  lParam: LPARAM): LRESULT; stdcall;
begin
  Result := 0;
  case Msg of
    WM_QUERYENDSESSION: ;  { Return zero to deny any shutdown requests }
    WM_KillFirstPhase: begin
        PostQuitMessage(0);
        { If we got WM_KillFirstPhase, the second phase must have been
          successful (up until now, at least). Set an exit code of 0. }
        UninstallExitCode := 0;
      end;
  else
    Result := CallWindowProc(OldWindowProc, Wnd, Msg, wParam, lParam);
  end;
end;

procedure DeleteUninstallDataFiles;
var
  ProcessWnd: HWND;
  ProcessID: DWORD;
  Process: THandle;
begin
  Log('Deleting Uninstall data files.');

  { Truncate the .dat file to zero bytes just before relinquishing exclusive
    access to it }
  try
    UninstLogFile.Seek(0);
    UninstLogFile.Truncate;
  except
    { ignore any exceptions, just in case }
  end;
  FreeAndNil(UninstLogFile);

  { Delete the .dat and .msg files }
  DeleteFile(UninstDataFile);
  DeleteFile(UninstMsgFile);

  { Tell the first phase to terminate, then delete its .exe }
  if FirstPhaseWnd <> 0 then begin
    if InitialProcessWnd <> 0 then
      { If the first phase respawned, wait on the initial process }
      ProcessWnd := InitialProcessWnd
    else
      ProcessWnd := FirstPhaseWnd;
    ProcessID := 0;
    if GetWindowThreadProcessId(ProcessWnd, @ProcessID) <> 0 then
      Process := OpenProcess(SYNCHRONIZE, False, ProcessID)
    else
      Process := 0;  { shouldn't get here }
    SendNotifyMessage(FirstPhaseWnd, WM_KillFirstPhase, 0, 0);
    if Process <> 0 then begin
      WaitForSingleObject(Process, INFINITE);
      CloseHandle(Process);
    end;
    { Sleep for a bit to allow pre-Windows 2000 Add/Remove Programs to finish
      bringing itself to the foreground before we take it back below. Also
      helps the DelayDeleteFile call succeed on the first try. }
    if not Debugging then
      Sleep(500);
  end;
  UninstallExitCode := 0;
  DelayDeleteFile(False, UninstExeFile, 13, 50, 250);
  if Debugging then
    DebugNotifyUninstExe('');
  { Pre-Windows 2000 Add/Remove Programs will try to bring itself to the
    foreground after the first phase terminates. Take it back. }
  Application.BringToFront;
end;

procedure ProcessCommandLine;
var
  WantToSuppressMsgBoxes: Boolean;
  I: Integer;
  ParamName, ParamValue: String;
begin
  WantToSuppressMsgBoxes := False;

  for I := 1 to NewParamCount do begin
    SplitNewParamStr(I, ParamName, ParamValue);
    if CompareText(ParamName, '/Log') = 0 then begin
      EnableLogging := True;
      LogFilename := '';
    end
    else
    if CompareText(ParamName, '/Log=') = 0 then begin
      EnableLogging := True;
      LogFilename := ParamValue;
    end
    else
    if CompareText(ParamName, '/INITPROCWND=') = 0 then begin
      DidRespawn := True;
      InitialProcessWnd := StrToInt(ParamValue);
    end
    else
    if CompareText(ParamName, '/SECONDPHASE=') = 0 then begin
      SecondPhase := True;
      UninstExeFile := ParamValue;
    end
    else if CompareText(ParamName, '/FIRSTPHASEWND=') = 0 then
      FirstPhaseWnd := StrToInt(ParamValue)
    else if CompareText(ParamName, '/SILENT') = 0 then
      Silent := True
    else if CompareText(ParamName, '/VERYSILENT') = 0 then
      VerySilent := True
    else if CompareText(ParamName, '/NoRestart') = 0 then
      NoRestart := True
    else if CompareText(ParamName, '/SuppressMsgBoxes') = 0 then
      WantToSuppressMsgBoxes := True
    else if CompareText(ParamName, '/DEBUGWND=') = 0 then
      DebugWnd := StrToInt(ParamValue);
  end;

  if WantToSuppressMsgBoxes and (Silent or VerySilent) then
    InitSuppressMsgBoxes := True;
end;

procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep; HandleException: Boolean);
begin
  if CodeRunner <> nil then begin
    try
      CodeRunner.RunProcedure('CurUninstallStepChanged', [Ord(CurUninstallStep)], False);
    except
      if HandleException then begin
        Log('CurUninstallStepChanged raised an exception.');
        ShowExceptionMsg;
      end
      else begin
        Log('CurUninstallStepChanged raised an exception (fatal).');
        raise;
      end;
    end;
  end;
end;

function OpenUninstDataFile(const AAccess: TFileAccess): TFile;
begin
  Result := nil;  { avoid warning }
  try
    Result := TFile.Create(UninstDataFile, fdOpenExisting, AAccess, fsNone);
  except
    on E: EFileError do begin
      SetLastError(E.ErrorCode);
      RaiseLastError(FmtSetupMessage1(msgUninstallOpenError,
        UninstDataFile));
    end;
  end;
end;

function RespawnFirstPhaseIfNeeded: Boolean;
var
  F: TFile;
  Flags: TUninstallLogFlags;
  RequireAdmin: Boolean;
begin
  Result := False;
  if DidRespawn then
    Exit;

  F := OpenUninstDataFile(faRead);
  try
    Flags := ReadUninstallLogFlags(F, UninstDataFile);
  finally
    F.Free;
  end;
  RequireAdmin := (ufAdminInstalled in Flags) or (ufPowerUserInstalled in Flags);

  if NeedToRespawnSelfElevated(RequireAdmin, False) then begin
    { Hide the taskbar button }
    SetWindowPos(Application.Handle, 0, 0, 0, 0, 0, SWP_NOSIZE or
      SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_HIDEWINDOW);
    try
      RespawnSelfElevated(UninstExeFile,
        Format('/INITPROCWND=$%x ', [Application.Handle]) + GetCmdTail,
        UninstallExitCode);
    except
      { Re-show the taskbar button and re-raise }
      if not(ExceptObject is EAbort) then
        ShowWindow(Application.Handle, SW_SHOW);
      raise;
    end;
    Result := True;
  end;
end;

procedure RunFirstPhase;
var
  TempFile: String;
  Wnd: HWND;
  ProcessHandle: THandle;
begin
  { Copy self to TEMP directory with a name like _iu14D2N.tmp. The
    actual uninstallation process must be done from somewhere outside
    the application directory since EXE's can't delete themselves while
    they are running. }
  if not GenerateNonRandomUniqueFilename(GetTempDir, TempFile) then
    try
      RestartReplace(False, TempFile, '');
    except
      { ignore exceptions }
    end;
  if not CopyFile(PChar(UninstExeFile), PChar(TempFile), False) then
    RaiseLastError(SetupMessages[msgLdrCannotCreateTemp]);
  { Don't want any attribute like read-only transferred }
  SetFileAttributes(PChar(TempFile), FILE_ATTRIBUTE_NORMAL);

  { Create first phase window. This window waits for a WM_KillFirstPhase
    message from the second phase process, and terminates itself in
    response. The reason the first phase doesn't just terminate
    immediately is because the Control Panel Add/Remove applet refreshes
    its list as soon as the program terminates. So it waits until the
    uninstallation is complete before terminating. }
  Wnd := CreateWindowEx(0, 'STATIC', '', 0, 0, 0, 0, 0, HWND_DESKTOP, 0,
    HInstance, nil);
  Longint(OldWindowProc) := SetWindowLong(Wnd, GWL_WNDPROC,
    Longint(@FirstPhaseWindowProc));
  try
    { Hide the application window so that we don't end up with two taskbar
      buttons once the second phase starts }
    SetWindowPos(Application.Handle, 0, 0, 0, 0, 0, SWP_NOSIZE or
      SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_HIDEWINDOW);

    { Execute the copy of itself ("second phase") }
    ProcessHandle := Exec(TempFile, Format('/SECONDPHASE="%s" /FIRSTPHASEWND=$%x ',
      [NewParamStr(0), Wnd]) + GetCmdTail);

    { Wait till the second phase process unexpectedly dies or is ready
      for the first phase to terminate. }
    repeat until ProcessMsgs or (MsgWaitForMultipleObjects(1,
      ProcessHandle, False, INFINITE, QS_ALLINPUT) <> WAIT_OBJECT_0+1);
    CloseHandle(ProcessHandle);
  finally
    DestroyWindow(Wnd);
  end;
end;

procedure AssignCustomMessages(AData: Pointer; ADataSize: Cardinal);

  procedure Corrupted;
  begin
    InternalError('Custom message data corrupted');
  end;

  procedure Read(var Buf; const Count: Cardinal);
  begin
    if Count > ADataSize then
      Corrupted;
    Move(AData^, Buf, Count);
    Dec(ADataSize, Count);
    Inc(Cardinal(AData), Count);
  end;

  procedure ReadString(var S: String);
  var
    N: Integer;
  begin
    Read(N, SizeOf(N));
    if (N < 0) or (N > $FFFFF) then  { sanity check }
      Corrupted;
    SetString(S, nil, N);
    if N <> 0 then
      Read(Pointer(S)^, N * SizeOf(S[1]));
  end;

var
  Count, I: Integer;
  CustomMessageEntry: PSetupCustomMessageEntry;
begin
  Read(Count, SizeOf(Count));
  Entries[seCustomMessage].Capacity := Count;
  for I := 0 to Count-1 do begin
    CustomMessageEntry := AllocMem(SizeOf(TSetupCustomMessageEntry));
    try
      ReadString(CustomMessageEntry.Name);
      ReadString(CustomMessageEntry.Value);
      CustomMessageEntry.LangIndex := -1;
    except
      SEFreeRec(CustomMessageEntry, SetupCustomMessageEntryStrings, SetupCustomMessageEntryAnsiStrings);
      raise;
    end;
    Entries[seCustomMessage].Add(CustomMessageEntry);
  end;
end;

function ExtractCompiledCodeText(S: String): AnsiString;
begin
{$IFDEF UNICODE}
  SetString(Result, PAnsiChar(Pointer(S)), Length(S)*SizeOf(S[1]));
{$ELSE}
  Result := S;
{$ENDIF}
end;

procedure RunSecondPhase;
const
  RemovedMsgs: array[Boolean] of TSetupMessageID =
    (msgUninstalledMost, msgUninstalledAll);
var
  RestartSystem: Boolean;
  CompiledCodeData: array[0..6] of String;
  CompiledCodeText: AnsiString;
  Res, RemovedAll, UninstallNeedsRestart: Boolean;
  StartTime: DWORD;
begin
  if VerySilent then
    SetTaskbarButtonVisibility(False);

  RestartSystem := False;
  AllowUninstallerShutdown := (WindowsVersion shr 16 >= $0600);

  try
    if DebugWnd <> 0 then
      SetDebugWnd(DebugWnd, True);

    if EnableLogging then begin
      try
        if LogFilename = '' then
          StartLogging('Uninstall')
        else
          StartLoggingWithFixedFilename(LogFilename);
      except
        on E: Exception do begin
          E.Message := 'Error creating log file:' + SNewLine2 + E.Message;
          raise;
        end;
      end;
    end;
    Log('Setup version: ' + SetupTitle + ' version ' + SetupVersion);
    Log('Original Uninstall EXE: ' + UninstExeFile);
    Log('Uninstall DAT: ' + UninstDataFile);
    Log('Uninstall command line: ' + GetCmdTail);
    LogWindowsVersion;

    { Open the .dat file for read access }
    UninstLogFile := OpenUninstDataFile(faRead);

    { Load contents of the .dat file }
    UninstLog := TExtUninstallLog.Create;
    UninstLog.Load(UninstLogFile, UninstDataFile);

    Title := FmtSetupMessage1(msgUninstallAppFullTitle, UninstLog.AppName);

    { If install was done in Win64, verify that we're still running Win64.
      This test shouldn't fail unless the user somehow downgraded their
      Windows version, or they're running an uninstaller from another machine
      (which they definitely shouldn't be doing). }
    if (ufWin64 in UninstLog.Flags) and not IsWin64 then begin
      LoggedAppMessageBox(PChar(SetupMessages[msgUninstallOnlyOnWin64]), PChar(Title),
        MB_OK or MB_ICONEXCLAMATION, True, IDOK);
      Abort;
    end;

    { Check if admin privileges are needed to uninstall }
    if (ufAdminInstalled in UninstLog.Flags) and not IsAdminLoggedOn then begin
      LoggedAppMessageBox(PChar(SetupMessages[msgOnlyAdminCanUninstall]), PChar(Title),
        MB_OK or MB_ICONEXCLAMATION, True, IDOK);
      Abort;
    end;

    { Reopen the .dat file for exclusive, read/write access and keep it
      open for the duration of the uninstall process to prevent a second
      instance of the same uninstaller from running. }
    FreeAndNil(UninstLogFile);
    UninstLogFile := OpenUninstDataFile(faReadWrite);

    if not UninstLog.ExtractLatestRecData(utCompiledCode,
         SetupBinVersion {$IFDEF UNICODE} or Longint($80000000) {$ENDIF}, CompiledCodeData) then
      InternalError('Cannot find utCompiledCode record for this version of the uninstaller');
    if DebugWnd <> 0 then
      CompiledCodeText := DebugClientCompiledCodeText
    else
      CompiledCodeText := ExtractCompiledCodeText(CompiledCodeData[0]);

{$IFNDEF UNICODE}
    { Initialize ConstLeadBytes }
    if Length(CompiledCodeData[1]) <> SizeOf(UninstLeadBytes) then
      InternalError('utCompiledCode[1] is invalid');
    Move(Pointer(CompiledCodeData[1])^, UninstLeadBytes, SizeOf(UninstLeadBytes));
    ConstLeadBytes := @UninstLeadBytes;
{$ENDIF}

    { Initialize install mode }
    if UninstLog.InstallMode64Bit then begin
      { Sanity check: InstallMode64Bit should never be set without ufWin64 }
      if not IsWin64 then
        InternalError('Install was done in 64-bit mode but not running 64-bit Windows now');
      Initialize64BitInstallMode(True);
    end
    else
      Initialize64BitInstallMode(False);

    { Create temporary directory and extract 64-bit helper EXE if necessary }
    CreateTempInstallDir;

    if CompiledCodeText <> '' then begin
      { Setup some global variables which are accessible to [Code] }

      InitMainNonSHFolderConsts;
      LoadSHFolderDLL;

      UninstallExeFilename := UninstExeFile;
      UninstallExpandedAppId := UninstLog.AppId;
      UninstallSilent := Silent or VerySilent;

      UninstallExpandedApp := CompiledCodeData[2];
      UninstallExpandedGroup := CompiledCodeData[3];
      UninstallExpandedGroupName := CompiledCodeData[4];
      UninstallExpandedLanguage := CompiledCodeData[5];
      AssignCustomMessages(Pointer(CompiledCodeData[6]), Length(CompiledCodeData[6])*SizeOf(CompiledCodeData[6][1]));

      CodeRunner := TScriptRunner.Create();
      CodeRunner.OnDllImport := CodeRunnerOnDllImport;
      CodeRunner.OnDebug := CodeRunnerOnDebug;
      CodeRunner.OnDebugIntermediate := CodeRunnerOnDebugIntermediate;
      CodeRunner.OnException := CodeRunnerOnException;
      CodeRunner.LoadScript(CompiledCodeText, DebugClientCompiledCodeDebugInfo);
    end;
    try
      try
        if CodeRunner <> nil then begin
          try
            Res := CodeRunner.RunBooleanFunction('InitializeUninstall', [''], False, True);
          except
            Log('InitializeUninstall raised an exception (fatal).');
            raise;
          end;
          if not Res then begin
            Log('InitializeUninstall returned False; aborting.');
            Abort;
          end;
        end;

        { Confirm uninstall }
        if not Silent and not VerySilent then begin
          if LoggedMessageBoxFmt1(msgConfirmUninstall, UninstLog.AppName, Title,
             MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON2, True, IDYES) <> IDYES then
            Abort;
        end;

        CurUninstallStepChanged(usAppMutexCheck, False);

        { Is the app running? }
        while UninstLog.CheckMutexes do
          { Yes, tell user to close it }
          if LoggedMessageBoxFmt1(msgUninstallAppRunningError, UninstLog.AppName, Title,
             MB_OKCANCEL or MB_ICONEXCLAMATION, True, IDCANCEL) <> IDOK then
            Abort;
   
        { Check for active WM_QUERYENDSESSION/WM_ENDSESSION }
        while AcceptedQueryEndSessionInProgress do
          LoggedAppMessageBox('Setup cannot continue at this time, shutdown negotiation is in progress. Click OK to retry.',
            PChar(Title), MB_OK or MB_ICONSTOP, False, 0);

        { Disable Uninstall shutdown }
        AllowUninstallerShutdown := False;
        ShutdownBlockReasonCreate(Application.Handle, FmtSetupMessage1(msgUninstallStatusLabel, UninstLog.AppName));

        { Create and show the progress form }
        InitializeUninstallProgressForm;

        CurUninstallStepChanged(usUninstall, False);

        { Start the actual uninstall process }
        StartTime := GetTickCount;
        UninstLog.FLastUpdateTime := StartTime;
        RemovedAll := UninstLog.PerformUninstall(True, DeleteUninstallDataFiles);

        LogFmt('Removed all? %s', [SYesNo[RemovedAll]]);

        UninstallNeedsRestart := UninstLog.NeedRestart or (ufAlwaysRestart in UninstLog.Flags);
        if (CodeRunner <> nil) and CodeRunner.FunctionExists('UninstallNeedRestart') then begin
          if not UninstallNeedsRestart then begin
            try
              if CodeRunner.RunBooleanFunction('UninstallNeedRestart', [''], False, False) then begin
                UninstallNeedsRestart := True;
                Log('Will restart because UninstallNeedRestart returned True.');
              end;
            except
              Log('UninstallNeedRestart raised an exception.');
              ShowExceptionMsg;
            end;
          end
          else
            Log('Not calling UninstallNeedRestart because a restart has already been deemed necessary.');
        end;

        LogFmt('Need to restart Windows? %s', [SYesNo[UninstallNeedsRestart]]);

        { Ensure at least 1 second has passed since the uninstall process
          began, then destroy the form }
        if not VerySilent then begin
          while True do begin
            Application.ProcessMessages;
            if Cardinal(GetTickCount - StartTime) >= Cardinal(1000) then
              Break;
            { Delay for 10 ms, or until a message arrives }
            MsgWaitForMultipleObjects(0, THandle(nil^), False, 10, QS_ALLINPUT);
          end;
        end;
        FreeAndNil(UninstallProgressForm);

        CurUninstallStepChanged(usPostUninstall, True);

        if not UninstallNeedsRestart then begin
          if not Silent and not VerySilent then
            LoggedMessageBoxFmt1(RemovedMsgs[RemovedAll], UninstLog.AppName,
              Title, MB_ICONINFORMATION or MB_OK or MB_SETFOREGROUND, True, IDOK);
        end
        else begin
          if not NoRestart then begin
            if VerySilent or
               (LoggedMessageBoxFmt1(msgUninstalledAndNeedsRestart, UninstLog.AppName,
                  Title, MB_ICONINFORMATION or MB_YESNO or MB_SETFOREGROUND, True, IDYES) = IDYES) then
              RestartSystem := True;
          end;
          if not RestartSystem then
            Log('Will not restart Windows automatically.');
        end;

        CurUninstallStepChanged(usDone, True);
      except
        { Show any pending exception here *before* DeinitializeUninstall
          is called, which could display an exception message of its own }
        ShowExceptionMsg;
        Abort;
      end;
    finally
      { Free the form here, too, in case an exception occurred above }
      try
        FreeAndNil(UninstallProgressForm);
      except
        ShowExceptionMsg;
      end;
      if CodeRunner <> nil then begin
        try
          CodeRunner.RunProcedure('DeinitializeUninstall', [''], False);
        except
          Log('DeinitializeUninstall raised an exception.');
          ShowExceptionMsg;
        end;
      end;
      ShutdownBlockReasonDestroy(Application.Handle);
    end;
  finally
    FreeAndNil(CodeRunner);
    UnloadSHFolderDLL;
    RemoveTempInstallDir;
    UninstLog.Free;
    FreeAndNil(UninstLogFile);
  end;

  if RestartSystem then begin
    if not Debugging then begin
      Log('Restarting Windows.');
      RestartInitiatedByThisProcess := True;
      if not RestartComputer then begin
        { If another app denied the shutdown, we probably lost the foreground;
          try to take it back. (Note: Application.BringToFront can't be used
          because we have no visible forms, and MB_SETFOREGROUND doesn't make
          the app's taskbar button blink.) }
        SetForegroundWindow(Application.Handle);
        LoggedAppMessageBox(PChar(SetupMessages[msgErrorRestartingComputer]),
          PChar(SetupMessages[msgErrorTitle]), MB_OK or MB_ICONEXCLAMATION,
          True, IDOK);
      end;
    end
    else
      Log('Not restarting Windows because Uninstall is being run from the debugger.');
  end;
end;

procedure RunUninstaller;
var
  F: TFile;
  UninstallerMsgTail: TUninstallerMsgTail;
begin
  { Set default title; it's set again below after the messages are read }
  Application.Title := 'Uninstall';
  { This is needed for D3+: Must force the application window visible since
    we aren't displaying any forms }
  ShowWindow(Application.Handle, SW_SHOW);

  try
    InitializeCommonVars;

    SetCurrentDir(GetSystemDir);

    UninstExeFile := NewParamStr(0);
    ProcessCommandLine;  { note: may change UninstExeFile }
    UninstDataFile := PathChangeExt(UninstExeFile, '.dat');
    UninstMsgFile := PathChangeExt(UninstExeFile, '.msg');

    { Initialize messages }
    F := TFile.Create(UninstExeFile, fdOpenExisting, faRead, fsRead);
    try
      F.Seek(F.Size.Lo - SizeOf(UninstallerMsgTail));
      F.ReadBuffer(UninstallerMsgTail, SizeOf(UninstallerMsgTail));
      if UninstallerMsgTail.ID <> UninstallerMsgTailID then begin
        { No valid UninstallerMsgTail record found at the end of the EXE;
          load messages from an external .msg file. }
        LoadSetupMessages(UninstMsgFile, 0, True);
      end
      else
        LoadSetupMessages(UninstExeFile, UninstallerMsgTail.Offset, True);
    finally
      F.Free;
    end;
    LangOptions.DialogFontName := MessagesLangOptions.DialogFontName;
    LangOptions.DialogFontSize := MessagesLangOptions.DialogFontSize;
    LangOptions.RightToLeft := lfRightToLeft in MessagesLangOptions.Flags;
    SetMessageBoxRightToLeft(LangOptions.RightToLeft);
    SetMessageBoxCaption(mbInformation, PChar(SetupMessages[msgInformationTitle]));
    SetMessageBoxCaption(mbConfirmation, PChar(SetupMessages[msgConfirmTitle]));
    SetMessageBoxCaption(mbError, PChar(SetupMessages[msgErrorTitle]));
    SetMessageBoxCaption(mbCriticalError, PChar(SetupMessages[msgErrorTitle]));
    Application.Title := SetupMessages[msgUninstallAppTitle];

    { Verify that uninstall data file exists }
    if not NewFileExists(UninstDataFile) then begin
      LoggedMessageBoxFmt1(msgUninstallNotFound, UninstDataFile,
        SetupMessages[msgUninstallAppTitle], MB_ICONSTOP or MB_OK, True, IDOK);
      Abort;
    end;

    if not SecondPhase then begin
      if not RespawnFirstPhaseIfNeeded then
        RunFirstPhase;
    end
    else
      RunSecondPhase;
  except
    ShowExceptionMsg;
  end;

  { Call EndDebug after all exception messages have been shown and logged in
    the IDE's Debug Output }
  EndDebug;

  Halt(UninstallExitCode);
end;

procedure HandleUninstallerEndSession;
begin
  { If second phase, remove the temp dir. The self copy made by the first phase will be
    deleted on restart. }
  if SecondPhase then begin
    Log('Detected restart. Removing temporary directory.');

    try
      UnloadSHFolderDLL;
      RemoveTempInstallDir;
    except
      ShowExceptionMsg;
    end;

    EndDebug;

    { Don't use Halt. See Setup.dpr }
    TerminateProcess(GetCurrentProcess, UninstallExitCode);
  end;
end;

end.
