Inno-Setup-issrc/Projects/Src/Setup.SpawnServer.pas

456 lines
14 KiB
ObjectPascal
Raw Permalink Normal View History

unit Setup.SpawnServer;
2011-10-06 20:53:09 +02:00
{
Inno Setup
Copyright (C) 1997-2024 Jordan Russell
2011-10-06 20:53:09 +02:00
Portions by Martijn Laan
For conditions of distribution and use, see LICENSE.TXT.
Spawn server
}
interface
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, Shared.Int64Em, PathFunc, Shared.CommonFunc, Setup.InstFunc, Setup.SpawnCommon;
2011-10-06 20:53:09 +02:00
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;
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
2024-03-27 17:03:31 +01:00
failure, 0 is returned. }
2011-10-06 20:53:09 +02:00
const
TokenElevationType = 18;
var
Token: THandle;
ElevationType: DWORD;
ReturnLength: DWORD;
begin
Result := 0;
2024-03-27 17:03:31 +01:00
if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then begin
2011-10-06 20:53:09 +02:00
ElevationType := 0;
2024-03-27 17:03:31 +01:00
if GetTokenInformation(Token, TTokenInformationClass(TokenElevationType),
2011-10-06 20:53:09 +02:00
@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;
2024-03-27 17:03:31 +01:00
if not IsAdminLoggedOn then begin
2011-10-06 20:53:09 +02:00
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: }
2024-03-27 17:03:31 +01:00
Result := True;
2011-10-06 20:53:09 +02:00
end;
{$ENDIF}
function GetFinalFileName(const Filename: String): String;
2024-03-27 17:03:31 +01:00
{ Calls GetFinalPathNameByHandle to expand any SUBST'ed drives, network drives,
and symbolic links in Filename. This is needed for elevation to succeed when
Setup is started from a SUBST'ed drive letter. }
2011-10-06 20:53:09 +02:00
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
2024-03-31 16:10:26 +02:00
GetFinalPathNameByHandleFunc: function(hFile: THandle; lpszFilePath: PWideChar;
2011-10-06 20:53:09 +02:00
cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
Attr, FlagsAndAttributes: DWORD;
H: THandle;
Res: Integer;
Buf: array[0..4095] of Char;
begin
GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
2024-03-27 17:03:31 +01:00
'GetFinalPathNameByHandleW');
2011-10-06 20:53:09 +02:00
if Assigned(GetFinalPathNameByHandleFunc) then begin
Attr := GetFileAttributes(PChar(Filename));
if Attr <> INVALID_FILE_ATTRIBUTES then begin
2011-10-06 20:53:09 +02:00
{ 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
2024-03-27 17:03:31 +01:00
elevated / running as administrator. If UAC is disabled, "runas"
2011-10-06 20:53:09 +02:00
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.
2024-03-27 17:03:31 +01:00
2. If AExeFilename is on a network drive, the ShellExecuteEx function is
smart enough to substitute it with a UNC path. }
2011-10-06 20:53:09 +02:00
const
SEE_MASK_NOZONECHECKS = $00800000;
var
ExpandedExeFilename, WorkingDir: String;
Info: TShellExecuteInfo;
WaitResult: DWORD;
begin
if not SameText(PathExtractExt(AExeFilename), '.exe') then
InternalError('Cannot respawn self, not named .exe');
2011-10-06 20:53:09 +02:00
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, nil, FResultCode);
2011-10-06 20:53:09 +02:00
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;
end.