unit OperationWithTimeout;

interface

uses
  Winapi.Windows, Winapi.ShlObj, Winapi.ShellAPI, ActiveX;

function SHGetFileInfoWithTimeout(
  pszPath: LPCWSTR; dwFileAttributes: DWORD; var psfi: TSHFileInfoW;
  cbFileInfo, uFlags: UINT; Timeout: Integer): DWORD_PTR;

function ShellFolderParseDisplayNameWithTimeout(
  ShellFolder: IShellFolder; hwndOwner: HWND; pbcReserved: Pointer; lpszDisplayName: POLESTR;
  out pchEaten: ULONG; out ppidl: PItemIDList; var dwAttributes: ULONG; Timeout: Integer): HResult;

function DestinationListBeginList(
  DestinationList: ICustomDestinationList; var pcMaxSlots: UINT; const riid: TIID; out ppv: Pointer; Timeout: Integer): HRESULT;

var
  TimeoutShellOperations: Boolean = True;

implementation

uses
  System.Classes, System.Types, System.SysUtils, CompThread;

type
  TOperation = class;

  TOperationEvent = procedure(Operation: TOperation);

  TOperation = class(TObject)
  public
    // SHGetFileInfoWithTimeout
    PIDL: PItemIDList;
    Path: string;
    dwFileAttributes: DWORD;
    psfi: TSHFileInfoW;
    cbFileInfo, uFlags: UINT;
    ResultDWordPtr: DWORD_PTR;

    // ShellFolderParseDisplayNameWithTimeout
    ShellFolder: IShellFolder;
    ResultHResult: HResult;
    hwndOwner: HWND;
    pbcReserved: Pointer;
    DisplayName: string;
    pchEaten: ULONG;
    ppidl: PItemIDList;
    dwAttributes: ULONG;

    // DestinationListBeginList uses ResultHResult
    DestinationList: ICustomDestinationList;
    pcMaxSlots: UINT;
    riid: TIID;
    ppv: Pointer;
  end;

type
  TOperationWithTimeoutThread = class(TCompThread)
  public
    constructor Create(Operation: TOperation; OperationEvent: TOperationEvent);

  protected
    procedure Execute; override;

  private
    FOperation: TOperation;
    FOperationEvent: TOperationEvent;
  end;

constructor TOperationWithTimeoutThread.Create(Operation: TOperation; OperationEvent: TOperationEvent);
begin
  inherited Create(True);
  FOperation := Operation;
  FOperationEvent := OperationEvent;
end;

procedure TOperationWithTimeoutThread.Execute;
begin
  // Needed for various API, particularly:
  // - SHGetFileInfo fails to return icon index on some systems;
  // - ICustomDestinationList.BeginList returns invalid "removed" array.
  CoInitialize(nil);
  FOperationEvent(FOperation);
end;

function WaitForOperation(
  Operation: TOperation; OperationEvent: TOperationEvent; Milliseconds: Cardinal): Boolean;
{$IFNDEF IDE}
var
  Thread: TOperationWithTimeoutThread;
{$ENDIF}
begin
// When running from IDE, it triggers starting/exiting the thread taking ages.
// So in IDE we revert to single-thread approach.
{$IFNDEF IDE}
  if not TimeoutShellOperations then
{$ENDIF}
  begin
    OperationEvent(Operation);
    Result := True;
  end
{$IFNDEF IDE}
    else
  begin
    // Have to start new thread for each request. When shared thread is used, it eventually hangs.
    // Most probably do to the fact that we violate COM threading model.
    // So using a new thread for each request, is only a hack that happens to work by pure luck.
    // We may want to use shared thread at least for COM-free operations, like SHGetFileInfo.
    Thread := TOperationWithTimeoutThread.Create(Operation, OperationEvent);
    Thread.Resume;
    Result := Thread.WaitFor(MSecsPerSec);
    if Result then
    begin
      Thread.Free;
    end
      else
    begin
      // There's a chance for memory leak, if thread is terminated
      // between WaitFor() and this line
      Thread.FreeOnTerminate := True;
    end;
  end;
{$ENDIF}
end;

procedure SHGetFileInfoOperation(Operation: TOperation);
var
  pszPath: LPCWSTR;
begin
  if Operation.uFlags and SHGFI_PIDL <> 0 then
  begin
    pszPath := LPCWSTR(Operation.PIDL);
  end
    else
  begin
    pszPath := LPCWSTR(Operation.Path);
  end;

  Operation.ResultDWordPtr :=
    SHGetFileInfo(pszPath, Operation.dwFileAttributes, Operation.psfi, Operation.cbFileInfo, Operation.uFlags);
end;

function SHGetFileInfoWithTimeout(
  pszPath: LPCWSTR; dwFileAttributes: DWORD; var psfi: TSHFileInfoW;
  cbFileInfo, uFlags: UINT; Timeout: Integer): DWORD_PTR;
var
  Operation: TOperation;
begin
  Operation := TOperation.Create;
  if uFlags and SHGFI_PIDL <> 0 then
  begin
    Operation.PIDL := PItemIDList(pszPath);
  end
    else
  begin
    Operation.Path := pszPath;
  end;
  Operation.dwFileAttributes := dwFileAttributes;
  Operation.psfi := psfi;
  Operation.cbFileInfo := cbFileInfo;
  Operation.uFlags := uFlags;
  if WaitForOperation(Operation, SHGetFileInfoOperation, Timeout) then
  begin
    psfi := Operation.psfi;
    Result := Operation.ResultDWordPtr;
    Operation.Free;
  end
    else
  begin
    FillChar(psfi, SizeOf(psfi), 0);
    Result := 0;
  end;
end;

procedure ShellFolderParseDisplayNameOperation(Operation: TOperation);
begin
  Operation.ResultHResult :=
    Operation.ShellFolder.ParseDisplayName(
      Operation.hwndOwner, Operation.pbcReserved, PChar(Operation.DisplayName),
      Operation.pchEaten, Operation.ppidl, Operation.dwAttributes);
end;

function ShellFolderParseDisplayNameWithTimeout(
  ShellFolder: IShellFolder; hwndOwner: HWND; pbcReserved: Pointer; lpszDisplayName: POLESTR; out pchEaten: ULONG;
  out ppidl: PItemIDList; var dwAttributes: ULONG; Timeout: Integer): HResult;
var
  Operation: TOperation;
begin
  Operation := TOperation.Create;
  Operation.ShellFolder := ShellFolder;
  Operation.hwndOwner := hwndOwner;
  Operation.pbcReserved := pbcReserved;
  Operation.DisplayName := lpszDisplayName;
  Operation.pchEaten := 0;
  Operation.ppidl := nil;
  Operation.dwAttributes := 0;
  if WaitForOperation(Operation, ShellFolderParseDisplayNameOperation, Timeout) then
  begin
    ppidl := Operation.ppidl;
    dwAttributes := Operation.dwAttributes;
    Result := Operation.ResultHResult;
    Operation.Free;
  end
    else
  begin
    ppidl := nil;
    dwAttributes := 0;
    Result := E_FAIL;
  end;
end;

procedure DestinationListBeginListOperation(Operation: TOperation);
begin
  Operation.ResultHResult := Operation.DestinationList.BeginList(Operation.pcMaxSlots, Operation.riid, Operation.ppv);
end;

function DestinationListBeginList(
  DestinationList: ICustomDestinationList; var pcMaxSlots: UINT; const riid: TIID; out ppv: Pointer; Timeout: Integer): HRESULT;
var
  Operation: TOperation;
begin
  Operation := TOperation.Create;
  Operation.DestinationList := DestinationList;
  Operation.pcMaxSlots := pcMaxSlots;
  Operation.riid := riid;
  Operation.ppv := ppv;
  if WaitForOperation(Operation, DestinationListBeginListOperation, Timeout) then
  begin
    pcMaxSlots := Operation.pcMaxSlots;
    ppv := Operation.ppv;
    Result := Operation.ResultHResult;
    Operation.Free;
  end
    else
  begin
    ppv := nil;
    Result := E_FAIL;
  end;
end;

end.
