unit OperationWithTimeout;

interface

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

function ShellFolderGetAttributesOfWithTimeout(
  ShellFolder: IShellFolder; cidl: UINT; var apidl: PItemIDList; var rgfInOut: UINT; Timeout: Integer): HResult;

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;

implementation

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

type
  TOperation = class;

  TOperationEvent = procedure(Operation: TOperation);

  TOperation = class(TObject)
  public
    OperationEvent: TOperationEvent;

    // ShellFolderGetAttributesOfWithTimeout
    ShellFolder: IShellFolder;
    cidl: UINT;
    apidl: PItemIDList;
    rgfInOut: UINT;
    Timeout: Integer;
    ResultHResult: HResult;

    // SHGetFileInfoWithTimeout
    PIDL: PItemIDList;
    Path: string;
    dwFileAttributes: DWORD;
    psfi: TSHFileInfoW;
    cbFileInfo, uFlags: UINT;
    ResultDWordPtr: DWORD_PTR;

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

    constructor Create(AOperationEvent: TOperationEvent);
  end;

constructor TOperation.Create(AOperationEvent: TOperationEvent);
begin
  OperationEvent := AOperationEvent;
end;

type
  TOperationWithTimeoutThread = class(TCompThread)
  public
    constructor Create;
    destructor Destroy; override;
    procedure Terminate; override;

    procedure Queue(Operation: TOperation);
    function WaitForOperation(Milliseconds: Cardinal): Boolean;
    procedure Remove(Operation: TOperation);

  protected
    procedure Execute; override;

  private
    FCriticalSection: TCriticalSection;
    FRequestEvent: THandle;
    FResultEvent: THandle;
    FQueue: TObjectList;
    FResults: TObjectList;
  end;

constructor TOperationWithTimeoutThread.Create;
begin
  inherited Create(True);
  FRequestEvent := CreateEvent(nil, False, False, nil);
  FResultEvent := CreateEvent(nil, False, False, nil);
  FCriticalSection := TCriticalSection.Create;
  FQueue := TObjectList.Create;
  FResults := TObjectList.Create;
  Resume;
end;

destructor TOperationWithTimeoutThread.Destroy;
begin
  inherited;
  FQueue.Free;
  FResults.Free;
  FCriticalSection.Free;
  CloseHandle(FRequestEvent);
  CloseHandle(FResultEvent);
end;

procedure TOperationWithTimeoutThread.Terminate;
begin
  inherited;
  SetEvent(FRequestEvent);
end;

procedure TOperationWithTimeoutThread.Execute;
var
  Operation: TOperation;
begin
  while WaitForSingleObject(FRequestEvent, INFINITE) = WAIT_OBJECT_0 do
  begin
    if Terminated then
    begin
      break;
    end
      else
    begin
      FCriticalSection.Enter;
      try
        Operation := TOperation(FQueue[0]);
        FQueue.Extract(Operation);
      finally
        FCriticalSection.Leave;
      end;

      Operation.OperationEvent(Operation);
      FResults.Add(Operation);
      SetEvent(FResultEvent);
    end;
  end;
end;

procedure TOperationWithTimeoutThread.Queue(Operation: TOperation);
begin
  FCriticalSection.Enter;
  try
    FQueue.Add(Operation);
  finally
    FCriticalSection.Leave;
  end;

  SetEvent(FRequestEvent);
end;

function TOperationWithTimeoutThread.WaitForOperation(Milliseconds: Cardinal): Boolean;
begin
  ResetEvent(FResultEvent);
  Result := (WaitForSingleObject(FResultEvent, Milliseconds) = WAIT_OBJECT_0);
end;

procedure TOperationWithTimeoutThread.Remove(Operation: TOperation);
begin
  FCriticalSection.Enter;
  try
    FResults.Remove(Operation);
  finally
    FCriticalSection.Leave;
  end;
end;

var
  Thread: TOperationWithTimeoutThread = nil;

procedure NeedThread;
begin
  if not Assigned(Thread) then
  begin
    Thread := TOperationWithTimeoutThread.Create;
  end;
end;

function WaitForOperation(Milliseconds: Cardinal): Boolean;
begin
  Result := Thread.WaitForOperation(Milliseconds);
  if not Result then
  begin
    // There's a chance for memory leak, if thread is terminated
    // between WaitFor() and this line
    Thread.FreeOnTerminate := True;
    Thread.Terminate;
    Thread := nil;
  end;
end;

procedure ShellFolderGetAttributesOfOperation(Operation: TOperation);
var
  ErrorMode: Word;
begin
  ErrorMode := SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOOPENFILEERRORBOX);
  try
    Operation.ResultHResult := Operation.ShellFolder.GetAttributesOf(Operation.cidl, Operation.apidl, Operation.rgfInOut);
  except
    Operation.ResultHResult := E_FAIL;
  end;
  SetErrorMode(ErrorMode);
end;

function ShellFolderGetAttributesOfWithTimeout(
  ShellFolder: IShellFolder; cidl: UINT; var apidl: PItemIDList; var rgfInOut: UINT; Timeout: Integer): HResult;
var
  Operation: TOperation;
begin
  NeedThread;
  Operation := TOperation.Create(ShellFolderGetAttributesOfOperation);
  Operation.ShellFolder := ShellFolder;
  Operation.cidl := cidl;
  Operation.apidl := apidl;
  Operation.rgfInOut := rgfInOut;
  Thread.Queue(Operation);
  if WaitForOperation(Timeout) then
  begin
    apidl := Operation.apidl;
    rgfInOut := Operation.rgfInOut;
    Result := Operation.ResultHResult;
    Thread.Remove(Operation);
  end
    else
  begin
    rgfInOut := 0;
    Result := E_FAIL;
  end;
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
  NeedThread;
  Operation := TOperation.Create(SHGetFileInfoOperation);
  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;
  Thread.Queue(Operation);
  if WaitForOperation(Timeout) then
  begin
    psfi := Operation.psfi;
    Result := Operation.ResultDWordPtr;
    Thread.Remove(Operation);
  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
  NeedThread;
  Operation := TOperation.Create(ShellFolderParseDisplayNameOperation);
  Operation.ShellFolder := ShellFolder;
  Operation.hwndOwner := hwndOwner;
  Operation.pbcReserved := pbcReserved;
  Operation.DisplayName := lpszDisplayName;
  Operation.pchEaten := 0;
  Operation.ppidl := nil;
  Operation.dwAttributes := 0;
  Thread.Queue(Operation);
  if WaitForOperation(Timeout) then
  begin
    ppidl := Operation.ppidl;
    dwAttributes := Operation.dwAttributes;
    Result := Operation.ResultHResult;
    Thread.Remove(Operation);
  end
    else
  begin
    ppidl := nil;
    dwAttributes := 0;
    Result := E_FAIL;
  end;
end;

initialization

finalization
  if Assigned(Thread) then
  begin
    Thread.Free;
  end;
end.
