-
-
Notifications
You must be signed in to change notification settings - Fork 961
/
Copy pathSpawnServer.pas
556 lines (515 loc) · 18.5 KB
/
SpawnServer.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
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.