(*
  Name:             utilexecute
  Copyright:        Copyright (C) SIL International.
  Documentation:    
  Description:      
  Create Date:      1 Jan 2013

  Modified Date:    23 Feb 2016
  Authors:          mcdurdin
  Related Files:    
  Dependencies:     

  Bugs:             
  Todo:             
  Notes:            
  History:          01 Jan 2013 - mcdurdin - I3631 - V9.0 - if svn commit fails, then cvscommit app needs to be aware of this.
                    16 Apr 2014 - mcdurdin - I4170 - V9.0 - Console execute in utilexecute.pas needs a temp copy of buffer to avoid write access violations
                    23 Feb 2016 - mcdurdin - I4983 - Starting a subprocess can fail due to constant buffer for CreateProcessW
*)
unit utilexecute;

interface

uses
  WinApi.ShellApi,
  WinApi.Windows;

type
  TUtilExecuteCallbackEvent = procedure(var Cancelled: Boolean) of object;
  TUtilExecuteCallbackWaitEvent = procedure(hProcess: THandle; var Waiting, Cancelled: Boolean) of object;

  TUtilExecute = class sealed
    class function Console(const cmdline, curdir: string; var FLogText: string; var ExitCode: Integer; FOnCallback: TUtilExecuteCallbackEvent = nil): Boolean; overload; static;   // I3631
    class function Console(const cmdline, curdir: string; var FLogText: string; FOnCallback: TUtilExecuteCallbackEvent = nil): Boolean; overload; static;   // I3631
    class function WaitForProcess(const cmdline, curdir: string; ShowWindow: Integer = SW_SHOWNORMAL; FOnCallback: TUtilExecuteCallbackWaitEvent = nil): Boolean; overload; static;
    class function WaitForProcess(const cmdline, curdir: string; var EC: Cardinal; ShowWindow: Integer = SW_SHOWNORMAL; FOnCallback: TUtilExecuteCallbackWaitEvent = nil): Boolean; overload; static;
    class function Shell(Handle: HWND; const process, curdir: string; const parameters: string = ''; ShowWindow: Integer = SW_SHOWNORMAL; const Verb: string = 'open'): Boolean; static;
    class function ShellCurrentUser(Handle: HWND; const process, curdir: string; const parameters: string = ''; ShowWindow: Integer = SW_SHOWNORMAL; const Verb: string = 'open'): Boolean; static;
    class function URL(const url: string): Boolean;

    class function CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean): Boolean; overload;
    class function CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean; var AExitCode: Cardinal): Boolean; overload;

    class function Execute(const cmdline, curdir: string; ShowWindow: Integer): Boolean; overload; static;
    class function Execute(const cmdline, curdir: string; ShowWindow: Integer; var pi: TProcessInformation): Boolean; overload; static;
  end;

implementation

uses
  System.SysUtils,

  Unicode;

class function TUtilExecute.Console(const cmdline, curdir: string; var FLogText: string; FOnCallback: TUtilExecuteCallbackEvent = nil): Boolean;
var
  ec: Integer;
begin
  Result := TUtilExecute.Console(cmdline, curdir, FLogText, ec, FOnCallback);   // I3631
end;

class function TUtilExecute.Console(const cmdline, curdir: string; var FLogText: string; var ExitCode: Integer; FOnCallback: TUtilExecuteCallbackEvent = nil): Boolean;   // I3631
var
  si: TStartupInfo;
  b, ec: DWord;
  cmdlinebuf: string;
  buf: array[0..512] of ansichar;  // I3310
  SecAttrs: TSecurityAttributes;
  hsoutread, hsoutwrite: THandle;
  hsinread, hsinwrite: THandle;
  pi: TProcessInformation;
  n: Integer;
  FCancelled: Boolean;
begin
  Result := False;
  FillChar(SecAttrs, SizeOf(SecAttrs), #0);
  SecAttrs.nLength              := SizeOf(SecAttrs);
  SecAttrs.lpSecurityDescriptor := nil;
  SecAttrs.bInheritHandle       := TRUE;
  if not CreatePipe(hsoutread, hsoutwrite, @SecAttrs, 0) then Exit;

  FillChar(SecAttrs, SizeOf(SecAttrs), #0);
  SecAttrs.nLength              := SizeOf(SecAttrs);
  SecAttrs.lpSecurityDescriptor := nil;
  SecAttrs.bInheritHandle       := TRUE;
  if not CreatePipe(hsinread, hsinwrite, @SecAttrs, 0) then
  begin
    CloseHandle(hsoutread);
    CloseHandle(hsoutwrite);
    Exit;
  end;

  { See support.microsoft.com kb 190351 }

  n := 0;
  FLogText := '';

  SetLength(cmdlinebuf, Length(cmdline));   // I4170
  StrCopy(PChar(cmdlinebuf), PChar(cmdline));   // I4170

  //Sreen.Cursor := crHourglass;
  try
    si.cb := SizeOf(TStartupInfo);
    si.lpReserved := nil;
    si.lpDesktop := nil;
    si.lpTitle := nil;
    si.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
    si.wShowWindow := SW_HIDE;
    si.cbReserved2 := 0;
    si.lpReserved2 := nil;
    si.hStdInput := hsinread;
    si.hStdOutput := hsoutwrite;
    si.hStdError := hsoutwrite;

    if CreateProcess(nil, PChar(cmdlinebuf),   // I4170
      nil, nil, True, NORMAL_PRIORITY_CLASS, nil, PChar(curdir), si, pi) then
    try
      if GetExitCodeProcess(pi.hProcess, ec) then
      begin
        while ec = STILL_ACTIVE do
        begin
          Sleep(20);
          Inc(n);
          if n = 30 then
          begin
            if Assigned(FOnCallback) then
            begin
              FCancelled := False;
              FOnCallback(FCancelled);
              if FCancelled then
              begin
                SetLastError(ERROR_CANCELLED);
                Exit;
              end;
            end;
            n := 0;
          end;
          PeekNamedPipe(hsoutread, nil, 0, nil, @b, nil);
          if b > 0 then
          begin
            ReadFile(hsoutread, buf, High(buf), b, nil);
            FLogText := FLogText + String_AtoU(Copy(buf, 1, b));
          end;
          if not GetExitCodeProcess(pi.hProcess, ec) then ec := 0;
        end;
      end;

      repeat
        PeekNamedPipe(hsoutread, nil, 0, nil, @b, nil);
        if b > 0 then
        begin
          ReadFile(hsoutread, buf, High(buf), b, nil);
          FLogText := FLogText + String_AtoU(Copy(buf, 1, b));
        end;
      until b = 0;

      ExitCode := ec;   // I3631

      Result := True;
    finally
      CloseHandle(pi.hProcess);
      CloseHandle(pi.hThread);
    end;
  finally
    CloseHandle(hsoutread);
    CloseHandle(hsoutwrite);
    CloseHandle(hsinread);
    CloseHandle(hsinwrite);
    //Screen.Cursor := crDefault;
  end;
end;

class function TUtilExecute.Shell(Handle: HWND; const process, curdir, parameters: string;
  ShowWindow: Integer; const Verb: string): Boolean;
begin
  Result := ShellExecute(Handle, PChar(Verb), Pchar(process), PChar(parameters), Pchar(curdir), ShowWindow) > 32;
end;

class function TUtilExecute.ShellCurrentUser(Handle: HWND; const process,
  curdir, parameters: string; ShowWindow: Integer; const Verb: string): Boolean;
begin
  // Use mysterious ways to get the shell process to launch. There are at least
  // three different ways to do this. The way we are using has been used by the
  // bootstrap for years so pretty sure it's robust!
  // Alternative: https://devblogs.microsoft.com/oldnewthing/20131118-00/?p=2643
  // Alternative: https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
  Result := CreateProcessAsShellUser(process, '"'+process+'" '+parameters, False);
end;

class function TUtilExecute.URL(const url: string): Boolean;
begin
  Result := ShellExecute(GetActiveWindow, nil, PChar(url), nil, nil, SW_SHOW) >= 32;
end;

class function TUtilExecute.WaitForProcess(const cmdline, curdir: string; ShowWindow: Integer; FOnCallback: TUtilExecuteCallbackWaitEvent): Boolean;
var
  ec: Cardinal;
begin
  Result := WaitForProcess(cmdline, curdir, ec, ShowWindow, FOnCallback);
end;

class function TUtilExecute.WaitForProcess(const cmdline, curdir: string; var EC: Cardinal; ShowWindow: Integer; FOnCallback: TUtilExecuteCallbackWaitEvent): Boolean;
var
  Waiting: Boolean;
  si: TStartupInfoW;
  pi: TProcessInformation;
  Cancelled: Boolean;
  buf: PChar;
begin
  Result := False;

  Waiting := True;
  si.cb := SizeOf(TStartupInfo);
  si.lpReserved := nil;
  si.lpDesktop := nil;
  si.lpTitle := nil;
  si.dwFlags := STARTF_USESHOWWINDOW;
  si.wShowWindow := ShowWindow;
  si.cbReserved2 := 0;
  si.lpReserved2 := nil;

  EC := $FFFFFFFF;

  buf := AllocMem((Length(cmdline)+1)*sizeof(Char));   // I4983
  try
    StrPCopy(buf, cmdline);   // I4983
    if CreateProcess(nil, buf,   // I4983
            nil, nil, True, NORMAL_PRIORITY_CLASS, nil, PWideChar(curdir), si, pi) then
    try
      while Waiting do
      begin
        if Assigned(FOnCallback) then
        begin
          Cancelled := False;
          FOnCallback(pi.hProcess, Waiting, Cancelled);
          if Cancelled then
          begin
            SetLastError(ERROR_CANCELLED);
            Exit;
          end;
        end
        else
        begin
          WaitForSingleObject(pi.hProcess, INFINITE);
          Waiting := False;
        end;
      end;

      if not GetExitCodeProcess(pi.hProcess, EC) then
        EC := $FFFFFFFF;

      Result := True;
    finally
      CloseHandle(pi.hProcess);
      CloseHandle(pi.hThread);
    end;
  finally
    FreeMem(buf);   // I4983
  end;
end;

class function TUtilExecute.Execute(const cmdline, curdir: string; ShowWindow: Integer): Boolean;
var
  pi: TProcessInformation;
begin
  Result := Execute(cmdline, curdir, ShowWindow, pi);
  if Result then
  begin
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
  end;
end;

class function TUtilExecute.Execute(const cmdline, curdir: string; ShowWindow: Integer; var pi: TProcessInformation): Boolean;
var
  si: TStartupInfoW;
  buf: PChar;
begin
  Result := False;

  si.cb := SizeOf(TStartupInfo);
  si.lpReserved := nil;
  si.lpDesktop := nil;
  si.lpTitle := nil;
  si.dwFlags := STARTF_USESHOWWINDOW;
  si.wShowWindow := ShowWindow;
  si.cbReserved2 := 0;
  si.lpReserved2 := nil;

  buf := AllocMem((Length(cmdline)+1)*sizeof(Char));
  try
    StrPCopy(buf, cmdline);
    if CreateProcess(nil, buf,
      nil, nil, True, NORMAL_PRIORITY_CLASS, nil, PWideChar(curdir),
      si, pi) then
    begin
      Result := True;
    end;
  finally
    FreeMem(buf);
  end;
end;

// Refactored from UCreateProcessAsShellUser

function GetShellWindow: HWND; stdcall; external user32;

type
  TCreateProcessWithTokenW = function(hToken: THANDLE; dwLogonFlags: DWORD; lpApplicationName: LPCWSTR; lpCommandLine: LPWSTR;
    dwCreationFlags: DWORD; lpEnvironment: Pointer; lpCurrentDirectory: LPCWSTR; lpStartupInfo: PSTARTUPINFOW;
    lpProcessInformation: PPROCESSINFORMATION): BOOL; stdcall;

var
  CreateProcessWithTokenW: TCreateProcessWithTokenW = nil;

class function TUtilExecute.CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean): Boolean;  // I2757
var
  ec: Cardinal;
begin
  Result := CreateProcessAsShellUser(process, cmdline, Wait, ec);
end;

class function TUtilExecute.CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean; var AExitCode: Cardinal): Boolean;  // I2757
var
  dwProcessId: Cardinal;
  hProcess, hShellProcessToken: THandle;
  hDesktopWindow: THandle;
  si: TStartupInfoW;
  pi: TProcessInformation;
  hPrimaryToken: NativeUInt;  // I3309
  hCurrentThreadToken: THandle;
  tkp: TTokenPrivileges;
  retlen: Cardinal;

  procedure DoWait;
  var
    Msg: TMsg;
    Waiting: Boolean;
  begin
    Waiting := True;
    while Waiting do
    begin
      case MsgWaitForMultipleObjects(1, pi.hProcess, FALSE, INFINITE, QS_ALLINPUT) of
        WAIT_OBJECT_0 + 1:
          while PeekMessageW(Msg, 0, 0, 0, PM_REMOVE) do
          begin
            TranslateMessage(Msg);
            DispatchMessage(Msg);
          end;
        WAIT_OBJECT_0, WAIT_ABANDONED_0:
          Waiting := False;
        else
          Waiting := False;
      end;
    end;
  end;

  function IsElevated: Boolean;
  var
    hToken: THandle;
    Elevation: TTokenElevation;
    cbSize: DWORD;
  begin
    if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, hToken) then
    try
      cbSize := sizeof(TTokenElevation);
      if GetTokenInformation(hToken, TokenElevation, @Elevation, sizeof(TTokenElevation), cbSize) then
        Exit(Elevation.TokenIsElevated <> 0);
    finally
      CloseHandle(hToken);
    end;
    Result := False;
  end;

const
  SE_INCREASE_QUOTA_NAME = 'SeIncreaseQuotaPrivilege';
  TOKEN_ADJUST_SESSIONID = $100;
begin
  Result := False;

  FillChar(si, SizeOf(si), 0);
  si.cb := SizeOf(si);

  FillChar(pi, SizeOf(pi), 0);

  if not IsElevated then
  begin
    if not CreateProcessW(PWideChar(process), PWideChar(cmdline), nil, nil, False, NORMAL_PRIORITY_CLASS, nil, nil, si, pi) then Exit;
  end
  else
  begin
    if not Assigned(CreateProcessWithTokenW) then Exit;

    hDesktopWindow := GetShellWindow;
    if hDesktopWindow = 0 then Exit;
    if GetWindowThreadProcessId(hDesktopWindow, dwProcessId) = 0 then Exit;

    if not ImpersonateSelf(SecurityImpersonation) then Exit;
    try
      if not LookupPrivilegeValue(nil, SE_INCREASE_QUOTA_NAME, tkp.Privileges[0].Luid) then Exit;

      tkp.PrivilegeCount := 1;  // one privilege to set
      tkp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;

      if not OpenThreadToken(GetCurrentThread, TOKEN_ADJUST_PRIVILEGES, False, hCurrentThreadToken) then Exit;
      try
        if not AdjustTokenPrivileges(hCurrentThreadToken, False, tkp, 0, nil, retlen) then Exit;

        hProcess := OpenProcess(PROCESS_QUERY_INFORMATION, False, dwProcessId);
        if hProcess = 0 then Exit;
        try
          if not OpenProcessToken(hProcess, TOKEN_DUPLICATE, hShellProcessToken) then Exit;
          try
            if not DuplicateTokenEx(hShellProcessToken, TOKEN_QUERY or TOKEN_ASSIGN_PRIMARY or TOKEN_DUPLICATE or TOKEN_ADJUST_DEFAULT or TOKEN_ADJUST_SESSIONID,
              nil, SecurityImpersonation, TokenPrimary, hPrimaryToken) then Exit;
            try
              if not CreateProcessWithTokenW(hPrimaryToken, 0, PWideChar(process), PWideChar(cmdline), NORMAL_PRIORITY_CLASS, nil, nil, @si, @pi) then Exit;
            finally
              CloseHandle(hPrimaryToken);
            end;
          finally
            CloseHandle(hShellProcessToken);
          end;
        finally
          CloseHandle(hProcess);
        end;
      finally
        CloseHandle(hCurrentThreadToken);
      end;
    finally
      RevertToSelf;
    end;
  end;

  Result := True;

  if Wait then  // I2757   // I4318
  begin
    DoWait;
    GetExitCodeProcess(pi.hProcess, AExitCode);
  end;

  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
end;

var
  hAdvApi32: THandle = 0;
initialization
  hAdvApi32 := LoadLibrary(advapi32);
  if hAdvApi32 <> 0 then
    CreateProcessWithTokenW := GetProcAddress(hAdvApi32, 'CreateProcessWithTokenW');
finalization
  if hAdvApi32 <> 0 then
    FreeLibrary(hAdvApi32);
end.
