456 lines
14 KiB
ObjectPascal
456 lines
14 KiB
ObjectPascal
unit Setup.SpawnServer;
|
|
|
|
{
|
|
Inno Setup
|
|
Copyright (C) 1997-2024 Jordan Russell
|
|
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;
|
|
|
|
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
|
|
failure, 0 is returned. }
|
|
const
|
|
TokenElevationType = 18;
|
|
var
|
|
Token: THandle;
|
|
ElevationType: DWORD;
|
|
ReturnLength: DWORD;
|
|
begin
|
|
Result := 0;
|
|
if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then begin
|
|
ElevationType := 0;
|
|
if GetTokenInformation(Token, TTokenInformationClass(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 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 := True;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function GetFinalFileName(const Filename: String): String;
|
|
{ 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. }
|
|
|
|
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: PWideChar;
|
|
cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
|
|
Attr, FlagsAndAttributes: DWORD;
|
|
H: THandle;
|
|
Res: Integer;
|
|
Buf: array[0..4095] of Char;
|
|
begin
|
|
GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
|
|
'GetFinalPathNameByHandleW');
|
|
if Assigned(GetFinalPathNameByHandleFunc) then begin
|
|
Attr := GetFileAttributes(PChar(Filename));
|
|
if Attr <> INVALID_FILE_ATTRIBUTES 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. 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, the ShellExecuteEx function is
|
|
smart enough to substitute it with a UNC path. }
|
|
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');
|
|
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);
|
|
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.
|