| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- unit SpawnServer;
- {
- Inno Setup
- Copyright (C) 1997-2010 Jordan Russell
- Portions by Martijn Laan
- For conditions of distribution and use, see LICENSE.TXT.
- Spawn server
- $jrsoftware: issrc/Projects/SpawnServer.pas,v 1.13 2010/04/17 19:30:25 jr Exp $
- }
- interface
- {$I VERSION.INC}
- uses
- Windows, SysUtils, Messages;
- type
- TSpawnServer = class
- private
- FWnd: HWND;
- FSequenceNumber: Word;
- FCallStatus: Word;
- FResultCode: Integer;
- FNotifyRestartRequested: Boolean;
- FNotifyNewLanguage: Integer;
- function HandleExec(const IsShellExec: Boolean; const ADataPtr: Pointer;
- const ADataSize: Cardinal): LRESULT;
- procedure WndProc(var Message: TMessage);
- public
- constructor Create;
- destructor Destroy; override;
- property NotifyNewLanguage: Integer read FNotifyNewLanguage;
- property NotifyRestartRequested: Boolean read FNotifyRestartRequested;
- property Wnd: HWND read FWnd;
- end;
- procedure EnterSpawnServerDebugMode;
- function NeedToRespawnSelfElevated(const ARequireAdministrator,
- AEmulateHighestAvailable: Boolean): Boolean;
- procedure RespawnSelfElevated(const AExeFilename, AParams: String;
- var AExitCode: DWORD);
- implementation
- { For debugging only; remove 'x' to enable the define: }
- {x$DEFINE SPAWNSERVER_RESPAWN_ALWAYS}
- uses
- Classes, Forms, ShellApi, Int64Em, PathFunc, CmnFunc2, InstFunc, SpawnCommon;
- type
- TPtrAndSize = record
- Ptr: ^Byte;
- Size: Cardinal;
- end;
- procedure ProcessMessagesProc;
- begin
- Application.ProcessMessages;
- end;
- function ExtractBytes(var Data: TPtrAndSize; const Bytes: Cardinal;
- var Value: Pointer): Boolean;
- begin
- if Data.Size < Bytes then
- Result := False
- else begin
- Value := Data.Ptr;
- Dec(Data.Size, Bytes);
- Inc(Data.Ptr, Bytes);
- Result := True;
- end;
- end;
- function ExtractLongint(var Data: TPtrAndSize; var Value: Longint): Boolean;
- var
- P: Pointer;
- begin
- Result := ExtractBytes(Data, SizeOf(Longint), P);
- if Result then
- Value := Longint(P^);
- end;
- function ExtractString(var Data: TPtrAndSize; var Value: String): Boolean;
- var
- Len: Longint;
- P: Pointer;
- begin
- Result := ExtractLongint(Data, Len);
- if Result then begin
- if (Len < 0) or (Len > $FFFF) then
- Result := False
- else begin
- Result := ExtractBytes(Data, Len * SizeOf(Value[1]), P);
- if Result then
- SetString(Value, PChar(P), Len);
- end;
- end;
- end;
- type
- TOSVersionInfoExW = record
- dwOSVersionInfoSize: DWORD;
- dwMajorVersion: DWORD;
- dwMinorVersion: DWORD;
- dwBuildNumber: DWORD;
- dwPlatformId: DWORD;
- szCSDVersion: array[0..127] of WideChar;
- wServicePackMajor: Word;
- wServicePackMinor: Word;
- wSuiteMask: Word;
- wProductType: Byte;
- wReserved: Byte;
- end;
- const
- VER_MINORVERSION = $0000001;
- VER_MAJORVERSION = $0000002;
- VER_SERVICEPACKMINOR = $0000010;
- VER_SERVICEPACKMAJOR = $0000020;
- VER_GREATER_EQUAL = 3;
- var
- VerSetConditionMaskFunc, VerifyVersionInfoWFunc: Pointer;
- { These are implemented in asm because Delphi 2 doesn't support functions that
- take 64-bit parameters or return a 64-bit result (in EDX:EAX) }
- procedure CallVerSetConditionMask(var dwlConditionMask: Integer64;
- dwTypeBitMask: DWORD; dwConditionMask: DWORD);
- asm
- push esi
- mov esi, eax // ESI = @dwlConditionMask
- push ecx // dwConditionMask
- push edx // dwTypeBitMask
- push dword ptr [esi+4] // dwlConditionMask.Hi
- push dword ptr [esi] // dwlConditionMask.Lo
- call VerSetConditionMaskFunc
- mov dword ptr [esi], eax // write dwlConditionMask.Lo
- mov dword ptr [esi+4], edx // write dwlConditionMask.Hi
- pop esi
- end;
- function CallVerifyVersionInfoW(const lpVersionInfo: TOSVersionInfoExW;
- dwTypeMask: DWORD; const dwlConditionMask: Integer64): BOOL;
- asm
- push dword ptr [ecx+4] // dwlConditionMask.Hi
- push dword ptr [ecx] // dwlConditionMask.Lo
- push edx // dwTypeMask
- push eax // lpVersionInfo
- call VerifyVersionInfoWFunc
- end;
- function IsReallyVista: Boolean;
- { Returns True if the OS is *really* Vista or later. VerifyVersionInfo is used
- because it appears to always check the true OS version number, whereas
- GetVersion(Ex) can return a fake version number (e.g. 5.x) if the program is
- set to run in compatibility mode, or if it is started by a program running
- in compatibility mode. }
- var
- ConditionMask: Integer64;
- VerInfo: TOSVersionInfoExW;
- begin
- Result := False;
- { These functions are present on Windows 2000 and later.
- NT 4.0 SP6 has VerifyVersionInfoW, but not VerSetConditionMask.
- Windows 9x/Me and early versions of NT 4.0 have neither. }
- if Assigned(VerSetConditionMaskFunc) and Assigned(VerifyVersionInfoWFunc) then begin
- ConditionMask.Lo := 0;
- ConditionMask.Hi := 0;
- { Docs say: "If you are testing the major version, you must also test the
- minor version and the service pack major and minor versions." }
- CallVerSetConditionMask(ConditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
- CallVerSetConditionMask(ConditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
- CallVerSetConditionMask(ConditionMask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
- CallVerSetConditionMask(ConditionMask, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL);
- FillChar(VerInfo, SizeOf(VerInfo), 0);
- VerInfo.dwOSVersionInfoSize := SizeOf(VerInfo);
- VerInfo.dwMajorVersion := 6;
- Result := CallVerifyVersionInfoW(VerInfo, VER_MAJORVERSION or
- VER_MINORVERSION or VER_SERVICEPACKMAJOR or VER_SERVICEPACKMINOR,
- ConditionMask);
- end;
- end;
- const
- TokenElevationTypeDefault = 1; { User does not have a split token (they're
- not an admin, or UAC is turned off) }
- TokenElevationTypeFull = 2; { Has split token, process running elevated }
- TokenElevationTypeLimited = 3; { Has split token, process not running
- elevated }
- function GetTokenElevationType: DWORD;
- { Returns token elevation type (TokenElevationType* constant). In case of
- failure (e.g. not running Vista), 0 is returned. }
- const
- TokenElevationType = 18;
- var
- Token: THandle;
- ElevationType: DWORD;
- ReturnLength: DWORD;
- begin
- Result := 0;
- if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY,
- {$IFNDEF Delphi3orHigher} @ {$ENDIF} Token) then begin
- ElevationType := 0;
- if GetTokenInformation(Token,
- {$IFDEF Delphi3orHigher} TTokenInformationClass {$ENDIF} (TokenElevationType),
- @ElevationType, SizeOf(ElevationType), ReturnLength) then
- Result := ElevationType;
- CloseHandle(Token);
- end;
- end;
- function NeedToRespawnSelfElevated(const ARequireAdministrator,
- AEmulateHighestAvailable: Boolean): Boolean;
- {$IFNDEF SPAWNSERVER_RESPAWN_ALWAYS}
- var
- ElevationType: DWORD;
- begin
- Result := False;
- if IsReallyVista and not IsAdminLoggedOn then begin
- if ARequireAdministrator then
- Result := True
- else if AEmulateHighestAvailable then begin
- { Emulate the "highestAvailable" requestedExecutionLevel: respawn if
- the user has a split token and the process isn't running elevated.
- (An inverted test for TokenElevationTypeLimited is used, so that if
- GetTokenElevationType unexpectedly fails or returns some value we
- don't recognize, we default to respawning.) }
- ElevationType := GetTokenElevationType;
- if (ElevationType <> TokenElevationTypeDefault) and
- (ElevationType <> TokenElevationTypeFull) then
- Result := True;
- end;
- end;
- end;
- {$ELSE}
- begin
- { For debugging/testing only: }
- Result := (Lo(GetVersion) >= 5);
- end;
- {$ENDIF}
- function GetFinalFileName(const Filename: String): String;
- { Calls GetFinalPathNameByHandle (new API in Vista) to expand any SUBST'ed
- drives, network drives, and symbolic links in Filename.
- This is needed for elevation to succeed on Windows Vista/7 when Setup is
- started from a SUBST'ed drive letter. }
- function ConvertToNormalPath(P: PChar): String;
- begin
- Result := P;
- if StrLComp(P, '\\?\', 4) = 0 then begin
- Inc(P, 4);
- if (PathStrNextChar(P) = P + 1) and (P[1] = ':') and PathCharIsSlash(P[2]) then
- Result := P
- else if StrLIComp(P, 'UNC\', 4) = 0 then begin
- Inc(P, 4);
- Result := '\\' + P;
- end;
- end;
- end;
- const
- FILE_SHARE_DELETE = $00000004;
- var
- GetFinalPathNameByHandleFunc: function(hFile: THandle;
- lpszFilePath: {$IFDEF UNICODE} PWideChar {$ELSE} PAnsiChar {$ENDIF};
- cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
- Attr, FlagsAndAttributes: DWORD;
- H: THandle;
- Res: Integer;
- Buf: array[0..4095] of Char;
- begin
- GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
- {$IFDEF UNICODE}
- 'GetFinalPathNameByHandleW'
- {$ELSE}
- 'GetFinalPathNameByHandleA'
- {$ENDIF} );
- if Assigned(GetFinalPathNameByHandleFunc) then begin
- Attr := GetFileAttributes(PChar(Filename));
- if Attr <> $FFFFFFFF then begin
- { Backup semantics must be requested in order to open a directory }
- if Attr and FILE_ATTRIBUTE_DIRECTORY <> 0 then
- FlagsAndAttributes := FILE_FLAG_BACKUP_SEMANTICS
- else
- FlagsAndAttributes := 0;
- { Use zero access mask and liberal sharing mode to ensure success }
- H := CreateFile(PChar(Filename), 0, FILE_SHARE_READ or FILE_SHARE_WRITE or
- FILE_SHARE_DELETE, nil, OPEN_EXISTING, FlagsAndAttributes, 0);
- if H <> INVALID_HANDLE_VALUE then begin
- Res := GetFinalPathNameByHandleFunc(H, Buf, SizeOf(Buf) div SizeOf(Buf[0]), 0);
- CloseHandle(H);
- if (Res > 0) and (Res < (SizeOf(Buf) div SizeOf(Buf[0])) - 16) then begin
- { ShellExecuteEx fails with error 3 on \\?\UNC\ paths, so try to
- convert the returned path from \\?\ form }
- Result := ConvertToNormalPath(Buf);
- Exit;
- end;
- end;
- end;
- end;
- Result := Filename;
- end;
- function GetFinalCurrentDir: String;
- var
- Res: Integer;
- Buf: array[0..MAX_PATH-1] of Char;
- begin
- DWORD(Res) := GetCurrentDirectory(SizeOf(Buf) div SizeOf(Buf[0]), Buf);
- if (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0])) then
- Result := GetFinalFileName(Buf)
- else begin
- RaiseFunctionFailedError('GetCurrentDirectory');
- Result := '';
- end;
- end;
- procedure RespawnSelfElevated(const AExeFilename, AParams: String;
- var AExitCode: DWORD);
- { Spawns a new process using the "runas" verb.
- Notes:
- 1. Despite the function's name, the spawned process may not actually be
- elevated / running as administrator on Vista. If UAC is disabled, "runas"
- behaves like "open". Also, if a non-admin user is a member of a special
- system group like Backup Operators, they can select their own user account
- at a UAC dialog. Therefore, it is critical that the caller include some
- kind of protection against respawning more than once.
- 2. If AExeFilename is on a network drive, Vista's ShellExecuteEx function is
- smart enough to substitute it with a UNC path. XP does not do this, which
- causes the function to fail with ERROR_PATH_NOT_FOUND because the new
- user doesn't retain the original user's drive mappings. }
- const
- SEE_MASK_NOZONECHECKS = $00800000;
- var
- ExpandedExeFilename, WorkingDir: String;
- Info: TShellExecuteInfo;
- WaitResult: DWORD;
- begin
- ExpandedExeFilename := GetFinalFileName(AExeFilename);
- WorkingDir := GetFinalCurrentDir;
- FillChar(Info, SizeOf(Info), 0);
- Info.cbSize := SizeOf(Info);
- Info.fMask := SEE_MASK_FLAG_NO_UI or SEE_MASK_FLAG_DDEWAIT or
- SEE_MASK_NOCLOSEPROCESS or SEE_MASK_NOZONECHECKS;
- Info.lpVerb := 'runas';
- Info.lpFile := PChar(ExpandedExeFilename);
- Info.lpParameters := PChar(AParams);
- Info.lpDirectory := PChar(WorkingDir);
- Info.nShow := SW_SHOWNORMAL;
- if not ShellExecuteEx(@Info) then begin
- { Don't display error message if user clicked Cancel at UAC dialog }
- if GetLastError = ERROR_CANCELLED then
- Abort;
- Win32ErrorMsg('ShellExecuteEx');
- end;
- if Info.hProcess = 0 then
- InternalError('ShellExecuteEx returned hProcess=0');
- { Wait for the process to terminate, processing messages in the meantime }
- try
- repeat
- ProcessMessagesProc;
- WaitResult := MsgWaitForMultipleObjects(1, Info.hProcess, False,
- INFINITE, QS_ALLINPUT);
- until WaitResult <> WAIT_OBJECT_0+1;
- if WaitResult = WAIT_FAILED then
- Win32ErrorMsg('MsgWaitForMultipleObjects');
- { Now that the process has exited, process any remaining messages.
- (If our window is handling notify messages (ANotifyWndPresent=False)
- then there may be an asynchronously-sent "restart request" message
- still queued if MWFMO saw the process terminate before checking for
- new messages.) }
- ProcessMessagesProc;
- if not GetExitCodeProcess(Info.hProcess, AExitCode) then
- Win32ErrorMsg('GetExitCodeProcess');
- finally
- CloseHandle(Info.hProcess);
- end;
- end;
- procedure EnterSpawnServerDebugMode;
- { For debugging purposes only: Creates a spawn server window, but does not
- start a new process. Displays the server window handle in the taskbar.
- Terminates when F11 is pressed. }
- var
- Server: TSpawnServer;
- begin
- Server := TSpawnServer.Create;
- try
- Application.Title := Format('Wnd=$%x', [Server.FWnd]);
- while True do begin
- ProcessMessagesProc;
- if (GetFocus = Application.Handle) and (GetKeyState(VK_F11) < 0) then
- Break;
- WaitMessage;
- end;
- finally
- Server.Free;
- end;
- Halt(1);
- end;
- { TSpawnServer }
- constructor TSpawnServer.Create;
- begin
- inherited;
- FNotifyNewLanguage := -1;
- FWnd := AllocateHWnd(WndProc);
- if FWnd = 0 then
- RaiseFunctionFailedError('AllocateHWnd');
- end;
- destructor TSpawnServer.Destroy;
- begin
- if FWnd <> 0 then
- DeallocateHWnd(FWnd);
- inherited;
- end;
- function TSpawnServer.HandleExec(const IsShellExec: Boolean;
- const ADataPtr: Pointer; const ADataSize: Cardinal): LRESULT;
- var
- Data: TPtrAndSize;
- EDisableFsRedir: Longint;
- EVerb, EFilename, EParams, EWorkingDir: String;
- EWait, EShowCmd: Longint;
- ClientCurrentDir, SaveCurrentDir: String;
- ExecResult: Boolean;
- begin
- { Recursive calls aren't supported }
- if FCallStatus = SPAWN_STATUS_RUNNING then begin
- Result := SPAWN_MSGRESULT_ALREADY_IN_CALL;
- Exit;
- end;
- Result := SPAWN_MSGRESULT_INVALID_DATA;
- Data.Ptr := ADataPtr;
- Data.Size := ADataSize;
- if IsShellExec then begin
- if not ExtractString(Data, EVerb) then Exit;
- end
- else begin
- if not ExtractLongint(Data, EDisableFsRedir) then Exit;
- end;
- if not ExtractString(Data, EFilename) then Exit;
- if not ExtractString(Data, EParams) then Exit;
- if not ExtractString(Data, EWorkingDir) then Exit;
- if not ExtractLongint(Data, EWait) then Exit;
- if not ExtractLongint(Data, EShowCmd) then Exit;
- if not ExtractString(Data, ClientCurrentDir) then Exit;
- if Data.Size <> 0 then Exit;
- Inc(FSequenceNumber);
- FResultCode := -1;
- FCallStatus := SPAWN_STATUS_RUNNING;
- try
- SaveCurrentDir := GetCurrentDir;
- try
- SetCurrentDir(ClientCurrentDir);
- Result := SPAWN_MSGRESULT_SUCCESS_BITS or FSequenceNumber;
- { Send back the result code now to unblock the client }
- ReplyMessage(Result);
- if IsShellExec then begin
- ExecResult := InstShellExec(EVerb, EFilename, EParams, EWorkingDir,
- TExecWait(EWait), EShowCmd, ProcessMessagesProc, FResultCode);
- end
- else begin
- ExecResult := InstExec(EDisableFsRedir <> 0, EFilename, EParams, EWorkingDir,
- TExecWait(EWait), EShowCmd, ProcessMessagesProc, FResultCode);
- end;
- if ExecResult then
- FCallStatus := SPAWN_STATUS_RETURNED_TRUE
- else
- FCallStatus := SPAWN_STATUS_RETURNED_FALSE;
- finally
- SetCurrentDir(SaveCurrentDir);
- end;
- finally
- { If the status is still SPAWN_STATUS_RUNNING here, then an unexpected
- exception must've occurred }
- if FCallStatus = SPAWN_STATUS_RUNNING then
- FCallStatus := SPAWN_STATUS_EXCEPTION;
- end;
- end;
- procedure TSpawnServer.WndProc(var Message: TMessage);
- var
- Res: LRESULT;
- begin
- case Message.Msg of
- WM_COPYDATA:
- begin
- try
- case TWMCopyData(Message).CopyDataStruct.dwData of
- CD_SpawnServer_Exec,
- CD_SpawnServer_ShellExec:
- begin
- Message.Result := HandleExec(
- TWMCopyData(Message).CopyDataStruct.dwData = CD_SpawnServer_ShellExec,
- TWMCopyData(Message).CopyDataStruct.lpData,
- TWMCopyData(Message).CopyDataStruct.cbData);
- end;
- end;
- except
- if ExceptObject is EOutOfMemory then
- Message.Result := SPAWN_MSGRESULT_OUT_OF_MEMORY
- else
- { Shouldn't get here; we don't explicitly raise any exceptions }
- Message.Result := SPAWN_MSGRESULT_UNEXPECTED_EXCEPTION;
- end;
- end;
- WM_SpawnServer_Query:
- begin
- Res := SPAWN_MSGRESULT_INVALID_SEQUENCE_NUMBER;
- if Message.LParam = FSequenceNumber then begin
- Res := SPAWN_MSGRESULT_INVALID_QUERY_OPERATION;
- case Message.WParam of
- SPAWN_QUERY_STATUS:
- Res := SPAWN_MSGRESULT_SUCCESS_BITS or FCallStatus;
- SPAWN_QUERY_RESULTCODE_LO:
- Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Lo;
- SPAWN_QUERY_RESULTCODE_HI:
- Res := SPAWN_MSGRESULT_SUCCESS_BITS or LongRec(FResultCode).Hi;
- end;
- end;
- Message.Result := Res;
- end;
- WM_USER + 150: begin
- { Got a SetupNotifyWnd message. (See similar handling in SetupLdr.dpr) }
- if Message.WParam = 10000 then
- FNotifyRestartRequested := True
- else if Message.WParam = 10001 then
- FNotifyNewLanguage := Message.LParam;
- end;
- else
- Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam,
- Message.LParam);
- end;
- end;
- var
- Kernel32Handle: HMODULE;
- initialization
- Kernel32Handle := GetModuleHandle(kernel32);
- VerSetConditionMaskFunc := GetProcAddress(Kernel32Handle, 'VerSetConditionMask');
- VerifyVersionInfoWFunc := GetProcAddress(Kernel32Handle, 'VerifyVersionInfoW');
- end.
|