Enum Attached Files

Above snippet demonstrate how to enumerate files openned by running programs on Windows.

Some file unlocker use that technique to find where a specific file is attached and then force processes using that file to release it handle (via code injection techniques). I will write an example in a future snippet thread.

Notice: At the bottom of that page, you will see a concreate example about how to use that unit.

Features

  • Support both 32 and 64bit process.
  • Doesn’t require any additional libraries than native Delphi libraries.
  • Support scanning : All process ; Single process; List of process.

Code

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//  Author:                                                                   //
//    ->  Jean-Pierre LESUEUR (@DarkCoderSc)                                  //
//        https://github.com/DarkCoderSc                                      //
//        https://gist.github.com/DarkCoderSc                                 //
//        https://www.phrozen.io/                                             //
//  License:                                                                  //
//    -> MIT                                                                  //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

unit UntEnumAttachedFiles;

interface

{$ALIGN ON}
{$MINENUMSIZE 4}

uses Windows, Classes, SysUtils, Generics.Collections, tlHelp32;

type
  TSystemHandleInformation = record
    ProcessId: ULONG;
    ObjectTypeNumber: UCHAR;
    Flags: UCHAR;
    Handle: USHORT;
    Object_: PVOID;
    GrantedAccess: ACCESS_MASK;
  end;
  PSystemHandleInformation = ^TSystemHandleInformation;

  TSystemHandleInformations = record
    HandleCount : ULONG;
    Handles     : array[0..0] of TSystemHandleInformation;
  end;
  PSystemHandleInformations = ^TSystemHandleInformations;

  TUnicodeString = record
    Length: USHORT;
    MaximumLength: USHORT;
    Buffer: PWideChar;
  end;

  OBJECT_TYPE_INFORMATION = record
    Name: TUnicodeString;
    ObjectCount: ULONG;
    HandleCount: ULONG;
    Reserved1: array[0..3] of ULONG;
    PeakObjectCount: ULONG;
    PeakHandleCount: ULONG;
    Reserved2: array[0..3] of ULONG;
    InvalidAttributes: ULONG;
    GenericMapping: GENERIC_MAPPING;
    ValidAccess: ULONG;
    Unknown: UCHAR;
    MaintainHandleDatabase: ByteBool;
    Reserved3: array[0..1] of UCHAR;
    PoolType: Byte;
    PagedPoolUsage: ULONG;
    NonPagedPoolUsage: ULONG;
  end;
  POBJECT_TYPE_INFORMATION = ^OBJECT_TYPE_INFORMATION;

  OBJECT_NAME_INFORMATION = record
    Name: TUnicodeString;
  end;
  POBJECT_NAME_INFORMATION = ^OBJECT_NAME_INFORMATION;

type
  TFileInfo = class
  private
    FFileName : String;
    FHandle   : THandle;
    FFileSize : Int64;

    {@M}
    procedure SetFileName(AValue : String);
  public
    {@C}
    constructor Create();

    {@G/S}
    property FileName : String  read FFileName write SetFileName;
    property Handle   : THandle read FHandle   write FHandle;

    {@G}
    property FileSize : Int64 read FFileSize;
  end;

  TEnumAttachedFiles = class
  private
    FItems : TObjectDictionary<Cardinal, TObjectList<TFileInfo>>;
    FNTDLL : THandle; // Required Library

    // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
    NtQuerySystemInformation : function (
                                          SystemInformationClass : Cardinal;
                                          SystemInformation : PVOID;
                                          SystemInformationLength : ULONG;
                                          ReturnLength : PULONG): ULONG; stdcall;

    // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryobject
    NtQueryObject : function (
                                ObjectHandle : THandle;
                                ObjectInformationClass : Cardinal;
                                ObjectInformation : PVOID;
                                ObjectInformationLength : ULONG;
                                ReturnLength : PULONG
                              ): ULONG; stdcall;

    // https://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FType%20independed%2FNtDuplicateObject.html
    NtDuplicateObject : function (
                                    SourceProcessHandle : THandle;
                                    SourceHandle : THandle;
                                    TargetProcessHandle : THandle;
                                    TargetHandle : PHANDLE;
                                    DesiredAccess : ACCESS_MASK;
                                    Attributes : ULONG;
                                    Options : ULONG
                                  ): ULONG; stdcall;

    {@M}
    function ReadHandleInformationByProcessBundle(AProcessId : Cardinal; var AHandles : TList<THandle>) : TObjectList<TFileInfo>;
    function EnumProcess(AFilterSameArch : Boolean = False) : TList<Cardinal>;

    function GetCount() : Cardinal;
  public
    {@C}
    constructor Create();
    destructor Destroy(); override;

    {@M}
    function Enum(AProcessList : TList<Cardinal>) : Integer; overload;
    function Enum(AProcessId : Cardinal) : Boolean; overload;
    function Enum() : Integer; overload;

    {@G}
    property Items : TObjectDictionary<Cardinal, TObjectList<TFileInfo>> read FItems;
    property TotalCount : Cardinal read GetCount;
  end;

implementation

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  ___Local___

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

function GetFileSize(AFileName : String) : Int64;
var AFileInfo : TWin32FileAttributeData;
begin
  result := 0;

  if NOT FileExists(AFileName) then begin
    exit;
  end;

  if NOT GetFileAttributesEx(PWideChar(AFileName), GetFileExInfoStandard, @AFileInfo) then begin
    exit;
  end;

  ///
  result := Int64(AFileInfo.nFileSizeLow) or Int64(AFileInfo.nFileSizeHigh shl 32);
end;

function IsProcessX64(AProcessId : Cardinal) : Boolean;
var AProcHandle   : THandle;
    AWow64Process : bool;

const PROCESS_QUERY_LIMITED_INFORMATION = $1000;
begin
  result := false;
  ///

  {
    If we are not in a 64Bit system then we are for sure in a 32Bit system
  }
  if (TOSVersion.Architecture = arIntelX86) then
    Exit();
  ///

  AProcHandle := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, AProcessId);
  if AProcHandle = 0 then
    Exit;
  try
    isWow64Process(AProcHandle, AWow64Process);
    ///

    result := (NOT AWow64Process);
  finally
    CloseHandle(AProcHandle);
  end;
end;

function PhysicalToVirtualPath(APath : String) : String;
var i          : integer;
    ADrive     : String;
    ABuffer    : array[0..MAX_PATH-1] of Char;
    ACandidate : String;
begin
  {$I-}
  for I := 0 to 25 do begin
    ADrive := Format('%s:', [Chr(Ord('A') + i)]);
    ///

    if (QueryDosDevice(PWideChar(ADrive), ABuffer, MAX_PATH) = 0) then
      continue;

    ACandidate := String(ABuffer).ToLower();

    if String(Copy(APath, 1, Length(ACandidate))).ToLower() = ACandidate then begin
      Delete(APath, 1, Length(ACandidate));

      result := Format('%s%s', [ADrive, APath]);
    end;
  end;
  {$I+}
end;

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  TEnumAttachedFiles

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

{-------------------------------------------------------------------------------
  ___constructor
-------------------------------------------------------------------------------}
constructor TEnumAttachedFiles.Create();
begin
  inherited Create();
  ///

  FItems := TObjectDictionary<Cardinal, TObjectList<TFileInfo>>.Create([doOwnsValues]);

  FNTDLL := LoadLibrary('NTDLL.DLL');

  {
    Acquire required API's from NTDLL.DLL
  }
  NtQuerySystemInformation := nil;
  NtQueryObject := nil;
  NtDuplicateObject := nil;
  if (FNTDLL <> 0) then begin
    @NtQuerySystemInformation := GetProcAddress(FNTDLL, 'NtQuerySystemInformation');
    @NtQueryObject := GetProcAddress(FNTDLL, 'NtQueryObject');
    @NtDuplicateObject := GetProcAddress(FNTDLL, 'NtDuplicateObject');
  end;
end;

{-------------------------------------------------------------------------------
  ___destroy
-------------------------------------------------------------------------------}
destructor TEnumAttachedFiles.Destroy();
begin
  if (FNTDLL <> 0) then
    FreeLibrary(FNTDLL);
  ///

  NtQuerySystemInformation := nil;
  NtQueryObject := nil;
  NtDuplicateObject := nil;

  if Assigned(FItems) then
    FreeAndNil(FItems);

  ///
  inherited Destroy();
end;

{-------------------------------------------------------------------------------
  Retrieve Information about each handles per owner process
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.ReadHandleInformationByProcessBundle(AProcessId : Cardinal; var AHandles : TList<THandle>) : TObjectList<TFileInfo>;
var i                      : integer;
    AObjectHandle          : THandle;
    pObjectTypeInformation : POBJECT_TYPE_INFORMATION;
    pObjectNameInformation : POBJECT_NAME_INFORMATION;
    AObjectName            : String;
    ARet                   : Cardinal;
    AProcessHandle         : THandle;
    AQueryHandle           : THandle;
    AFileInfo              : TFileInfo;
    ARequiredSize          : DWORD;
    AFileName              : String;
begin
  result := nil;
  ///

  if NOT Assigned(AHandles) then
    Exit();

  AObjectHandle := 0;

  AProcessHandle := OpenProcess(
                                  (PROCESS_DUP_HANDLE or PROCESS_QUERY_INFORMATION or PROCESS_VM_READ),
                                  False,
                                  AProcessId
  );

  if (AProcessHandle = INVALID_HANDLE_VALUE) then
    Exit();

  try
    result := TObjectList<TFileInfo>.Create(True);
    ///

    for I := 0 to AHandles.Count -1 do begin
      ARet := NTDuplicateObject(
                                  AProcessHandle,
                                  AHandles.Items[I], // Current Handle
                                  GetCurrentProcess(),
                                  @AObjectHandle,
                                  0,
                                  0,
                                  0
      );

      if (ARet <> 0) then
        Continue;

      ///
      try
        // Get Required Length before doing memory allocation
        AQueryHandle := NtQueryObject(AObjectHandle, 2 {ObjectTypeInformation}, nil, 0, @ARequiredSize);
        if (ARequiredSize <= 0) then
          continue;
        ///

        GetMem(pObjectTypeInformation, ARequiredSize);
        try
          {
            Query Object Type
          }
          AQueryHandle := NtQueryObject(
                                          AObjectHandle,
                                          2 {ObjectTypeInformation},
                                          pObjectTypeInformation,
                                          ARequiredSize,
                                          nil
          );

          if (AQueryHandle <> 0) then
            Continue;

          {
            Filter for files handles only
          }
          if NOT String(pObjectTypeInformation^.Name.Buffer).ToUpper.StartsWith('FILE') then
            continue;

        finally
          FreeMem(pObjectTypeInformation, ARequiredSize);
        end;

        {
          Query Object Name (Should be Sizeof(TObjectNameInformation));
        }
        pObjectNameInformation := nil;

        AQueryHandle := NTQueryObject(
                                        AObjectHandle,
                                        1 {ObjectNameInformation},
                                        nil,
                                        0,
                                        @ARequiredSize
        );

        if (ARequiredSize <= 0) then
          continue;

        pObjectNameInformation := AllocMem(ARequiredSize);
        try
          AQueryHandle := NTQueryObject(
                                          AObjectHandle,
                                          1 {ObjectNameInformation},
                                          pObjectNameInformation,
                                          ARequiredSize,
                                          NIL
          );


          if (AQueryHandle <> 0) then
            Continue;

          AObjectName := String(pObjectNameInformation^.Name.Buffer).Trim();
          if (length(AObjectName) <= 0) then
            Continue;

          AFileName := PhysicalToVirtualPath(AObjectName);

          if NOT FileExists(AFileName) then
            continue;

          {
            Register new file information object
          }
          AFileInfo := TFileInfo.Create();

          AFileInfo.FileName := AFileName;
          AFileInfo.Handle   := AHandles.Items[I];

          {
            Push
          }
          result.Add(AFileInfo);
        finally
          FreeMem(pObjectNameInformation, ARequiredSize);
        end;
      finally
        CloseHandle(AObjectHandle);
      end;
    end;
  finally
    CloseHandle(AProcessHandle);
  end;
end;

{-------------------------------------------------------------------------------
  Enumerate Open Handles (Type = Open Files)
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.Enum(AProcessList : TList<Cardinal>) : Integer;
var AQueryHandle              : THandle;
    pHandleInformations       : PSystemHandleInformations;
    pCurrentHandleInformation : PSystemHandleInformation;
    ARequiredSize             : DWORD;
    I                         : Integer;
    ARet                      : Cardinal;
    AHandles                  : TObjectDictionary<Cardinal, TList<THandle>>;
    AHandleList               : TList<THandle>;
    AFileInfoList             : TObjectList<TFileInfo>;

const BASE_SIZE = 1024;
begin
  result := -99; // Unknwon
  ///

  self.FItems.Clear();

  {
    We must have access to those API's
  }
  if (NOT Assigned(NtQuerySystemInformation)) or
      (NOT Assigned(NtQueryObject)) or
      (NOT Assigned(NtDuplicateObject))
  then
    Exit();
  ///

  ARequiredSize := 0;

  ARequiredSize := BASE_SIZE;

  pHandleInformations := AllocMem(ARequiredSize);
  try
    {
      Retrieve open handles informations.
      Notice: Between two NTQuerySystemInformation calls, required size could increase
      resulting to another STATUS_INFO_LENGTH_MISMATCH error. Multiple NTQuerySystemInformation
      call could be required until we succeed.
    }
    while true do begin
      AQueryHandle := NTQuerySystemInformation(
                                                  16 {SystemHandleInformation},
                                                  pHandleInformations,
                                                  ARequiredSize,
                                                  @ARequiredSize
      );

      case AQueryHandle of
        ULONG($C0000004) {STATUS_INFO_LENGTH_MISMATCH} : begin
          ReallocMem(pHandleInformations, ARequiredSize);
        end;

        0 :
          break;

        else
          Exit(-1);
      end;
    end;

    {
      Enumerate handle, and sort them by it owner process ID
    }
    AHandles := TObjectDictionary<Cardinal, TList<THandle>>.Create([doOwnsValues]);
    try
      for I := 0 to (pHandleInformations^.HandleCount -1) do begin
        {$IFNDEF WIN64}
          pCurrentHandleInformation := pointer(Integer(@pHandleInformations^.Handles) + (I * SizeOf(TSystemHandleInformation)));
        {$ELSE}
          pCurrentHandleInformation := pointer(Int64(@pHandleInformations^.Handles) + (I * SizeOf(TSystemHandleInformation)));
        {$ENDIF}

        {
          Ignore out of scope processes
        }
        if NOT AProcessList.Contains(pCurrentHandleInformation.ProcessId) then
          continue;

        {
          Filter some access value and file types which was identified as causing problems
        }
        ARet := GetFileType(pCurrentHandleInformation^.Handle);

        if (ARet <> FILE_TYPE_DISK) and (ARet <> FILE_TYPE_UNKNOWN) then
          Continue;

        if (pCurrentHandleInformation^.GrantedAccess = $0012019F) or
           (pCurrentHandleInformation^.GrantedAccess = $001A019F) or
           (pCurrentHandleInformation^.GrantedAccess = $00120189) or
           (pCurrentHandleInformation^.GrantedAccess = $00100000) then
              continue;

        AHandleList := nil;

        if AHandles.ContainsKey(pCurrentHandleInformation^.ProcessId) then begin
          if NOT AHandles.TryGetValue(pCurrentHandleInformation^.ProcessId, AHandleList) then
            continue;
        end else begin
          AHandleList := TList<THandle>.Create();

          AHandles.Add(pCurrentHandleInformation^.ProcessId, AHandleList);
        end;

        if Assigned(AHandleList) then
          AHandleList.Add(pCurrentHandleInformation^.Handle);
      end;

      {
        Final Step is to retrieve additional information about each captured handle
        per owner process ID
      }
      for I in AHandles.Keys do begin
        if NOT AHandles.TryGetValue(I {Process ID}, AHandleList) then
            continue;
        ///

        AFileInfoList := ReadHandleInformationByProcessBundle(I, AHandleList);

        if Assigned(AFileInfoList) then begin
          if (AFileInfoList.Count > 0) then
            FItems.Add(I, AFileInfoList);
        end;
      end;

      ///
      result := 0; // Error Success
    finally
      if Assigned(AHandles) then
        FreeAndNil(AHandles);
    end;
  finally
    FreeMem(pHandleInformations, ARequiredSize);
  end;
end;

{-------------------------------------------------------------------------------
  Enumerate All Process for Open / Attached Files
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.Enum() : Integer; // Result = Number of scanned Process
var AList : TList<Cardinal>;
begin
  AList := EnumProcess(True);
  if Assigned(AList) then begin
    try
      if (AList.Count > 0) then
        Enum(AList);
    finally
      FreeAndNil(AList);
    end;
  end;
end;

{-------------------------------------------------------------------------------
  Enumerate a single process
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.Enum(AProcessId : Cardinal) : Boolean;
var AList : TList<Cardinal>;
begin
  AList := TList<Cardinal>.Create();
  try
    AList.Add(AProcessId);

    result := (self.Enum(AList) = 0);
  finally
    if Assigned(AList) then
      FreeAndNil(AList);
  end;
end;

{-------------------------------------------------------------------------------
  Get Total Attached Files Count
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.GetCount() : Cardinal;
var AProcessId : Cardinal;
    AFiles     : TObjectList<TFileInfo>;
begin
  result := 0;
  ///

  for AProcessId in FItems.Keys do begin
    if NOT FItems.TryGetValue(AProcessId, AFiles) then
      continue;
    ///

    if Assigned(AFiles) then
      Inc(result, AFiles.Count);
  end;
end;

{-------------------------------------------------------------------------------
  Enumerate Running Process (Compatible for Enum(ProcessList))
-------------------------------------------------------------------------------}
function TEnumAttachedFiles.EnumProcess(AFilterSameArch : Boolean = False) : TList<Cardinal>;
var ASnap         : THandle;
    AProcessEntry : TProcessEntry32;
    AProcessName  : String;

    procedure AppendEntry();
    begin
      if AFilterSameArch and ((IsProcessX64(GetCurrentProcessId())) <> (IsProcessX64(AProcessEntry.th32ProcessID))) then
        Exit();
//      ///

      result.Add(AProcessEntry.th32ProcessID);
    end;

begin
  result := TList<Cardinal>.Create();
  ///

  ASnap := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if ASnap = INVALID_HANDLE_VALUE then
    Exit();
  try
    ZeroMemory(@AProcessEntry, SizeOf(TProcessEntry32));
    ///

    AProcessEntry.dwSize := SizeOf(TProcessEntry32);

    if NOT Process32First(ASnap, AProcessEntry) then
      Exit();

    AppendEntry();

    while True do begin
      ZeroMemory(@AProcessEntry, SizeOf(TProcessEntry32));
      ///

      AProcessEntry.dwSize := SizeOf(TProcessEntry32);

      if NOT Process32Next(ASnap, AProcessEntry) then
        break;

      AppendEntry();
    end;
  finally
    CloseHandle(ASnap);
  end;
end;

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  TFileInfo

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

constructor TFileInfo.Create();
begin
  inherited Create();
  ///

  FFileName := '';
  FHandle   := INVALID_HANDLE_VALUE;
  FFileSize := 0;
end;

procedure TFileInfo.SetFileName(AValue: string);
begin
  FFileName := AValue;

  FFileSize := GetFileSize(AValue);
end;

end.

Usage Example (Console Application)

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//  Author:                                                                   //
//    ->  Jean-Pierre LESUEUR (@DarkCoderSc)                                  //
//        https://github.com/DarkCoderSc                                      //
//        https://www.phrozen.io/                                             //
//  License:                                                                  //
//    -> MIT                                                                  //
//                                                                            //
//  Description:                                                              //
//    -> Demonstrate how to use UntEnumAttachedFiles.pas                      //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

program ListAttachedFiles;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows, System.SysUtils, UntEnumAttachedFiles, Generics.Collections;

var AEnum      : TEnumAttachedFiles;
    AProcessId : Integer;
    AFiles     : TObjectList<TFileInfo>;
    I          : Integer;
    AFileInfo  : TFileInfo;

{-------------------------------------------------------------------------------
  Get Process Name (>= Vista)

  https://www.phrozen.io/snippets/2020/03/get-process-name-method-1-delphi/
-------------------------------------------------------------------------------}
function GetProcessName(AProcessID : Cardinal) : String;
var hProc      : THandle;
    ALength    : DWORD;
    hDLL       : THandle;

    QueryFullProcessImageNameW : function(
                                            AProcess: THANDLE;
                                            AFlags: DWORD;
                                            AFileName: PWideChar;
                                            var ASize: DWORD): BOOL; stdcall;

const PROCESS_QUERY_LIMITED_INFORMATION = $00001000;
begin
  result := '';
  ///

  if (TOSVersion.Major < 6) then
    Exit();
  ///

  QueryFullProcessImageNameW := nil;

  hDLL := LoadLibrary('kernel32.dll');
  if hDLL = 0 then
    Exit();
  try
    @QueryFullProcessImageNameW := GetProcAddress(hDLL, 'QueryFullProcessImageNameW');
    ///

    if Assigned(QueryFullProcessImageNameW) then begin
      hProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, AProcessID);
      if hProc = 0 then exit;
      try
        ALength := (MAX_PATH * 2);

        SetLength(result, ALength);

        if NOT QueryFullProcessImageNameW(hProc, 0, @result[1], ALength) then
          Exit();

        SetLength(result, ALength); // Get rid of extra junk
      finally
        CloseHandle(hProc);
      end;
    end;
  finally
    FreeLibrary(hDLL);
  end;
end;

{-------------------------------------------------------------------------------
  Display Program Help banner and optionally an error message.
-------------------------------------------------------------------------------}
procedure ShowHelp(AErrorMessage : String = '');
begin
  if Length(AErrorMessage) > 0 then begin
    WriteLn('');
    WriteLn('Error: ' + AErrorMessage);
  end;

  WriteLn('');
  WriteLn('Usage:');
  WriteLn('-a : List all attached files (System Wide).');
  WriteLn('-p <pid> : List all attached files from target process id.');
  WriteLn('');
end;

begin
  try
    if (ParamCount <= 0) or (ParamCount > 2) then begin
      raise Exception.Create('');
    end else begin
      AEnum := TEnumAttachedFiles.Create();
      try
        if (ParamCount = 1) and (ParamStr(1) = '-a') then
          AEnum.Enum() // Enumerate All
        else if (ParamStr(1) = '-p') then begin
          if TryStrToInt(ParamStr(2), AProcessId) then
            AEnum.Enum(AProcessId)
          else
            raise Exception.Create('Invalid Process Id (Must be numerical and >= 0).');
        end else
          raise Exception.Create('Invalid option.');

        {
          Display Grabbed Files
        }
        for AProcessId in AEnum.Items.Keys do begin
          if NOT AEnum.Items.TryGetValue(AProcessId, AFiles) then
            continue;
          ///

          Writeln('----------------------------------------------------------');
          Writeln(Format('%s (%d)', [ExtractFileName(GetProcessName(AProcessId)), AProcessId]));
          Writeln(ExtractFilePath(GetProcessName(AProcessId)));
          Writeln(Format('File Count : %d', [AFiles.Count]));
          Writeln('----------------------------------------------------------');

          for I := 0 to AFiles.Count -1 do begin
            AFileInfo := AFiles.Items[I];

            if NOT Assigned(AFileInfo) then
              continue;

            Writeln(Format('* [%s] %s', [IntToHex(AFileInfo.Handle, 8), AFileInfo.FileName]));
          end;

          Writeln('----------------------------------------------------------');
          Writeln('');
        end;
      finally
        if Assigned(AEnum) then
          FreeAndNil(AEnum);
      end;
    end;
  except
    on E: Exception do
      ShowHelp(E.Message);
  end;
end.

How it looks like

Screenshot

comments powered by Disqus