About SystemHandleInformation on 64 bits application - delphi

I need know how enumarate handles on 64 bits applicatio, i made it on32 bits and works perfectly, but the same code compiled as 64 bits only show some handles.
i already changed variables to longword for example but without success.
i read about SystemHandleInformation on x64 should be another value instead $10 (16 dec) but tried without success.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
Windows,
Classes,
PsApi;
const
SystemHandleInformation = $10;
STATUS_SUCCESS = $00000000;
STATUS_BUFFER_OVERFLOW = $80000005;
STATUS_INFO_LENGTH_MISMATCH = $C0000004;
//
type
NTSTATUS = Cardinal;
OBJECT_INFORMATION_CLASS = (ObjectBasicInformation, ObjectNameInformation,
ObjectTypeInformation, ObjectAllTypesInformation, ObjectHandleInformation);
//
SYSTEM_HANDLE = packed record
UniqueProcessId : USHORT;
CreatorBackTraceIndex : USHORT;
ObjectTypeIndex : UCHAR;
HandleAttributes : UCHAR;
HandleValue : USHORT;
HObject : PVOID;
GrantedAccess : ULONG;
end;
PSYSTEM_HANDLE = ^SYSTEM_HANDLE;
SYSTEM_HANDLE_ARRAY = Array[0..0] of SYSTEM_HANDLE;
PSYSTEM_HANDLE_ARRAY = ^SYSTEM_HANDLE_ARRAY;
//
SYSTEM_HANDLE_INFORMATION = packed record
uCount : ULONG;
Handles : SYSTEM_HANDLE_ARRAY;
end;
PSYSTEM_HANDLE_INFORMATION = ^SYSTEM_HANDLE_INFORMATION;
//
TNtQuerySystemInformation = function (SystemInformationClass:DWORD; SystemInformation:pointer; SystemInformationLength:DWORD; ReturnLength:PDWORD):THandle; stdcall;
TNtQueryObject = function (ObjectHandle:cardinal; ObjectInformationClass:OBJECT_INFORMATION_CLASS; ObjectInformation:pointer; Length:ULONG;ResultLength:PDWORD):THandle;stdcall;
var
NTQueryObject : TNtQueryObject;
NTQuerySystemInformation : TNtQuerySystemInformation;
Procedure EnumerateOpenFiles();
const
HANDLE_BUFFER_INCREASE_CHUNK = 5000 * 1024;
var
sDummy : string;
hProcess : THandle;
hObject : THandle;
ResultLength: DWORD;
aBufferSize : DWORD;
aIndex : LONG;//Integer;
pHandleInfo : PSYSTEM_HANDLE_INFORMATION;
HDummy : THandle;
lpszProcess : PWideChar;
begin
AbufferSize := HANDLE_BUFFER_INCREASE_CHUNK;
pHandleInfo := AllocMem(AbufferSize);
HDummy := NTQuerySystemInformation(DWORD(SystemHandleInformation), pHandleInfo, AbufferSize, #ResultLength); //Get the list of handles
if(HDummy = STATUS_SUCCESS) then
begin
for aIndex:=0 to pHandleInfo^.uCount-1 do
begin
hProcess := OpenProcess(PROCESS_DUP_HANDLE or PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, FALSE, pHandleInfo.Handles[aIndex].UniqueProcessId); //open the process to get aditional info
if(hProcess <> INVALID_HANDLE_VALUE) then
begin
hObject := 0;
if DuplicateHandle(hProcess, pHandleInfo.Handles[aIndex].HandleValue, GetCurrentProcess(), #hObject, STANDARD_RIGHTS_REQUIRED, FALSE, 0) then //Get a copy of the original handle
begin
lpszProcess := AllocMem(MAX_PATH);
if GetModuleFileNameEx(hProcess, 0,lpszProcess, MAX_PATH) <> 0 then
sDummy:=lpszProcess
else
sDummy:= 'System Process';
WriteLn(Format('PID [%d] Process [%s]', [pHandleInfo.Handles[aIndex].UniqueProcessId, sDummy]));
FreeMem(lpszProcess);
CloseHandle(hObject);
end;
CloseHandle(hProcess);
end;
end;
end;
WriteLn('Finish');
FreeMem(pHandleInfo);
end;
begin
NTQueryObject := GetProcAddress(GetModuleHandle('NTDLL.DLL'), 'NtQueryObject');
NTQuerySystemInformation := GetProcAddress(GetModuleHandle('NTDLL.DLL'), 'NtQuerySystemInformation');
if (#NTQuerySystemInformation <> nil) and (#NTQuerySystemInformation <> nil) then EnumerateOpenFiles() else WriteLn('falhou no inicio');
ReadLn;
end.
That works perfectly on x86 application, but when i change to x64 he don't show the same results as x86, anyone know why?

Local variable names and two unremoved comments suggest that this is a variation on code posted by RRUZ at 2009 here. At that time there was no 64 bit Delphi version so it was not possible for him to test the code on 64 bits. Anyway, I was able to test this with XE2 on W7x64 using "jwanative.pas" for the missing NtQuerySystemInformation from your sample. You also have one end too many, you need to remove the end that comes before FreeMem(lpszProcess);. Otherwise the code will not compile - probably a copy/paste error on your part.
The error is mis-packing the SYSTEM_HANDLE and SYSTEM_HANDLE_INFORMATION records, their layouts are messed up for 64 bit when packed. This page by Geoff Chappell (have to acknowledge according to the site's terms) suggests that
The SYSTEM_HANDLE_INFORMATION is 0x14 and 0x20 bytes in 32-bit and
64-bit Windows, respectively.
Unpack it to have 32 bytes in x64 instead of 28 while packed.
Similarly, this page suggests:
The SYSTEM_HANDLE_TABLE_ENTRY_INFO structure is 0x10 or 0x18 bytes in
32-bit and 64-bit Windows, respectively.
Unpack your record and it will be 24 bytes on x64 instead of 20 while packed. Although the members slightly differ, you'll be able to see it runs about the same as on x32.
Note that the code may or may not run on later/future versions of OS. Microsoft not only does not fully document system information retrieval but also warn that
The NtQuerySystemInformation function and the structures that it
returns are internal to the operating system and subject to change
from one release of Windows to another.

Related

Print the monitor's name with EnumDisplayDevices in Delphi

I need to read some information regarding the monitors connected through the EnumDisplayDevicesA function.
I tried to convert the following example written in c++ to delphi, but I have a problem when I try to read the device name from the PDISPLAY_DEVICEA structure LDeviceName := LDisplayDevice.deviceName; as it only returns Chinese characters.
I think it is a problem related to character encoding but I don't know how to fix it.
My source code:
program Monitor;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
const
user32 = 'user32.dll';
type
LONG = LongInt;
BOOL = LongBool;
PDISPLAY_DEVICE = ^DISPLAY_DEVICE;
LPCSTR = array[0..128 - 1] of WideChar;
PLPCSTR = ^LPCSTR;
//https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-display_devicea
DISPLAY_DEVICE = packed record
cb: Cardinal;
deviceName: array[0..32 - 1] of WideChar;
deviceString: array[0..128 - 1] of WideChar;
stateFlags: Cardinal;
deviceID: array[0..128 - 1] of WideChar;
deviceKey: array[0..128 - 1] of WideChar;
end;
//https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaydevicesa
function EnumDisplayDevicesA(APCSTR: PLPCSTR; iDevNum: Cardinal; PDISPLAY_DEVICEA: PDISPLAY_DEVICE; dwFlags: Cardinal): BOOL; stdcall; external user32;
procedure PrintMonitorNames();
var
LDisplayDevice: DISPLAY_DEVICE;
LDeviceIndex: Integer;
LMonitorIndex: Integer;
LDeviceName: string;
begin
LDisplayDevice.cb := Sizeof(LDisplayDevice);
LDeviceIndex := 0;
while EnumDisplayDevicesA(nil, LDeviceIndex, #LDisplayDevice, 0) do
begin
LDeviceName := LDisplayDevice.deviceName;
Writeln('Device name: ' + LDeviceName);
LMonitorIndex := 0;
while EnumDisplayDevicesA(#LDeviceName, LMonitorIndex, #LDisplayDevice, 0) do
begin
Writeln(StrPas(LDisplayDevice.deviceName) + ' ' + StrPas(LDisplayDevice.deviceString));
Inc(LMonitorIndex);
end;
Inc(LDeviceIndex);
end;
end;
var
LDummy: string;
begin
Writeln('START');
PrintMonitorNames();
Writeln('FINISH');
Readln(LDummy);
end.
You are mixing ANSI and Unicode.
The EnumDisplayDevices function exists in two versions:
EnumDisplayDevicesA which is (legacy) ANSI.
EnumDisplayDevicesW which is Unicode.
You are calling the ANSI version EnumDisplayDevicesA, but are using a Unicode version of DISPLAY_DEVICE. So you need to use EnumDisplayDevicesW instead.
This phenomenon that an API function exists in both W and A versions is present everywhere in the Windows API, so the above remarks are very general.
The fact that you get Chinese text because of this encoding mismatch is also very well known.
Having said all this, you don't need to declare EnumDisplayDevices yourself at all. Everything you need is already present in the Delphi RTL's Windows.pas unit, just like I showed you two days ago:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
Winapi.Windows;
begin
var dd, md: TDisplayDevice;
FillChar(dd, SizeOf(dd), 0);
dd.cb := SizeOf(dd);
FillChar(md, SizeOf(md), 0);
md.cb := SizeOf(md);
var i := 0;
while EnumDisplayDevices(nil, i, dd, 0) do
begin
var j := 0;
while EnumDisplayDevices(#dd.DeviceName[0], j, md, 0) do
begin
Writeln(md.DeviceString);
Inc(j);
end;
Inc(i);
end;
Readln;
end.
Notice that MSDN says this:
The winuser.h header defines EnumDisplayDevices as an alias which automatically selects the ANSI or Unicode version of this function based on the definition of the UNICODE preprocessor constant.
The same remarks applies to the Delphi RTL's Windows.pas.

IcmpSendEcho (ping) fails on Windows 10

We have a legacy Delphi application which uses IcmpSendEcho (from iphlpapi.dll) to perform echo requests. As I understand it, this performs the same function as "ping" from the command prompt.
On Windows XP, the code below works fine. When the IPv4 address is correct the response is quick and if not error code 11010 (IP_REQ_TIMED_OUT) is returned as expected.
However, on my 32-bit Windows 10 machine, the error code is 87 (ERROR_INVALID_PARAMETER). I've carefully reviewed the Microsoft documentation for IcmpSendEcho and cannot see anything obvious that is wrong.
"ping 200.1.2.121" (the example IPv4 address I use in the code sample) works as expected from the command prompt in both XP and 10.
type
PIpAddress = ^TIpAddress;
TIpAddress = record
case Integer of
0: (S_un_b: TSunB);
1: (S_un_w: TSunW);
2: (S_addr: LongWord);
end;
IpAddress = TIpAddress;
// Functions imported from external DLLs
function IcmpCreateFile() : THandle; stdcall; external 'iphlpapi.dll';
function IcmpCloseHandle(icmpHandle: THandle) : Boolean; stdcall; external 'iphlpapi.dll';
function IcmpSendEcho(IcmpHandle: THandle; ipDest: IpAddress;
pRequestData: Pointer; nRequestSize: SmallInt; RequestOptions: Pointer;
pReplyBuffer: Pointer; dwReplySize: DWORD; dwTimeout: DWORD) : DWORD; stdcall; external 'iphlpapi.dll';
procedure TranslateStringToIpAddress(strIP: String; var ipAddress);
var
phe: PHostEnt;
pac: PChar;
begin
try
phe := GetHostByName(PChar(strIP));
if (Assigned(phe)) then
begin
pac := phe^.h_addr_list^;
if (Assigned(pac)) then
begin
with TIpAddress(ipAddress).S_un_b do
begin
by1 := Byte(pac[0]);
by2 := Byte(pac[1]);
by3 := Byte(pac[2]);
by4 := Byte(pac[3]);
end;
end
else
begin
raise Exception.Create('Error getting IP from HostName');
end;
end
else
begin
raise Exception.Create('Error getting HostName');
end;
except
FillChar(ipAddress, SizeOf(ipAddress), #0);
end;
end;
function Ping(strIpAddress : String) : Boolean;
const
ICMP_ECHO_BUFFER = 128; // Works as low as 28 on Windows XP (nothing works on Windows 10)
var
address: IpAddress;
dwReplies: DWORD;
{$IFDEF DBG} dwErrorCode: DWORD; {$ENDIF}
abyReplyBuffer: array[1..ICMP_ECHO_BUFFER] of BYTE;
begin
// Use this function to determine if an IPv4 address can be reached
Result := False;
// "m_cache.hPingHandle" is generated earlier with a call to "IcmpCreateFile"
if (m_cache.hPingHandle = INVALID_HANDLE_VALUE) then
Exit;
TranslateStringToIpAddress(strIpAddress, address);
dwReplies := IcmpSendEcho(
m_cache.hPingHandle, address, nil, 0, nil, #abyReplyBuffer, ICMP_ECHO_BUFFER, 0);
{$IFDEF DBG}
if (dwReplies = 0) then
begin
dwErrorCode := GetLastError();
// dwErrorCode = 87 (ERROR_INVALID_PARAMETER, "The parameter is incorrect")
Application.MessageBox(
PAnsiChar(Format('WinError = %d', [dwErrorCode])), 'Ping failed', MB_ICONEXCLAMATION);
end;
{$ENDIF}
// Success?
Result := (dwReplies <> 0);
end;
// Usage elsewhere in the application...
Ping('200.1.2.121'); // Works on Windows XP, but fails on Windows 10
Based on the comment from #FredS (thanks!), the answer is simply to make the last parameter for the IcmpSendEcho non-zero (eg. "200").
The MSDN documentation for IcmpSendEcho does not make this clear, so Microsoft have probably changed the internal implementation of this method from the version in Windows XP so that a non-zero Timeout is now required.

Delphi - On Screen Keyboard (osk.exe) works on Win32 but fails on Win64

I'm trying to run the on screen keyboard from my application. It works correctly under Windows XP 32 bits, but incorrect under Win 7 64 bits.
unit Unit5;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ShellAPI;
type
TForm5 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
class function IsWOW64: Boolean;
{ Public declarations }
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
procedure TForm5.FormCreate(Sender: TObject);
var path:String;
res : Integer;
function GetSysDir: string;
var
Buf: array[0..MAX_PATH] of Char;
Len: UINT;
S: String;
begin
{$IFNDEF WIN64}
if TForm5.IsWOW64 then
begin
Len := GetWindowsDirectory(Buf, MAX_PATH);
if Len = 0 then RaiseLastOSError;
SetString(S, Buf, Len);
Result := IncludeTrailingPathDelimiter(S) + 'Sysnative\';
Exit;
end;
{$ENDIF}
Len := GetSystemDirectory(Buf, MAX_PATH);
if Len = 0 then RaiseLastOSError;
SetString(S, Buf, Len);
Result := IncludeTrailingPathDelimiter(S);
end;
begin
path := GetSysDir;
path := path + 'osk.exe';
res := ShellExecute(self.Handle,'open',Pchar(path),nil,nil,SW_NORMAL);
if res <> 42 then
begin
ShowMessage(path);
RaiseLastOSError;
end;
end;
class function TForm5.IsWOW64: Boolean;
type
TIsWow64Process = function( // Type of IsWow64Process API fn
Handle: THandle;
var Res: BOOL
): BOOL; stdcall;
var
IsWow64Result: BOOL; // result from IsWow64Process
IsWow64Process: TIsWow64Process; // IsWow64Process fn reference
begin
// Try to load required function from kernel32
IsWow64Process := GetProcAddress(
GetModuleHandle('kernel32'), 'IsWow64Process'
);
if Assigned(IsWow64Process) then
begin
// Function is implemented: call it
if not IsWow64Process(GetCurrentProcess, IsWow64Result) then
RaiseLastOSError;
// Return result of function
Result := IsWow64Result;
end
else
// Function not implemented: can't be running on Wow64
Result := False;
end;
end.
Running the application under x64 reveals the path C:\Windows\Sysnative\osk.exe , and raise a 'call to an OS function failed' error.
Searching on windows directories reveals that osk.exe exists
There is something special about osk under UAC. This code fails with error code 740, ERROR_ELEVATION_REQUIRED, The requested operation requires elevation.
var
si: TStartupInfo;
pi: TProcessInformation;
....
si.cb := SizeOf(si);
GetStartupInfo(si);
Win32Check(CreateProcess('C:\Windows\system32\osk.exe', nil, nil, nil,
False, 0, nil, nil, si, pi));
This fails under both 32 and 64 bit processes on machines with UAC. You can find some discussion of the issue here: https://web.archive.org/web/20170311141004/http://blog.delphi-jedi.net/2008/05/17/the-case-of-shellexecute-shellexecuteex-createprocess-and-oskexe/
So your problem is not related to 32 or 64 bit, rather it is down to your XP system not having UAC.
More broadly I think this should be enough to convince you never to call ShellExecute again. It only exists for 16 bit compatibility and is singularly useless at reporting errors. If you want errors call ShellExecuteEx. However, since we are starting a new process, CreateProcess would normally be the right API to call.
That said, in this specific case the design of osk is such that it cannot be started programmatically by CreateProcess. It does need to be invoked by ShellExecute, or ShellExecuteEx. This allows the shell to perform its UAC magic. Now, it turns out that magic cannot happen from a 32 bit WOW64 process. The solution then is to start osk from a 64 bit process with a call to ShellExecuteEx.
Here is your workaround:
On a 32 bit system, you can simply call ShellExecuteEx to open osk.
On a 64 bit system, if your process is 64 bit, you can again call ShellExecuteEx to open osk.
On a 64 bit system, if your process is 32 bit WOW64 process, you need to start a separate 64 bit process which in turn calls ShellExecuteEx to open osk.
Since you don't appear to be using a 64 bit version of Delphi, you'll need to find a 64 bit compiler. You could use the 64 bit fpc, or a 64 bit C++ compiler. The following C++ program is enough:
#include <Windows.h>
#include <Shellapi.h>
int CALLBACK WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
SHELLEXECUTEINFOW sei = { sizeof(sei) };
sei.lpVerb = L"open";
sei.lpFile = L"osk.exe";
sei.nShow = SW_SHOW;
ShellExecuteExW(&sei);
}
You can compile that with a 64 bit C++ compiler and then call it from your 32 bit WOW64 process. Long winded I know, but it does have the merit of actually working!
Function Wow64DisableWow64FsRedirection(Var Wow64FsEnableRedirection: LongBool): LongBool; StdCall;
External 'Kernel32.dll' Name 'Wow64DisableWow64FsRedirection';
Var
Wow64FsEnableRedirection: LongBool;
begin
if Wow64DisableWow64FsRedirection(Wow64FsEnableRedirection) then ShellExecute(0,nil, 'osk.exe', nil, nil, SW_show);
end;
Another, much simpler, option is to use SysNative in ShellExecute as follows:
{
If UTExistFile(GetWindowsSystemDir() + '\OSK.exe') then
// we can "see" OSK.exe in the System32 folder, so we are running on
// 32-bit Windows, so no problem accessing OSK.EXE in System32.
ShellExecute(Application.Handle, // HWND hwnd
'open', // LPCTSTR lpOperation
LPCTSTR(GetWindowsSystemDir() + '\OSK.exe'), // LPCTSTR lpFile
'', // LPCTSTR lpParameters
'', // LPCTSTR lpDirectory,
SW_Show) // INT nShowCmd
else
// Use SysNative to get at OSK.EXE. This will not work for 64-bit OS
// before Vista (e.g. XP), but it won't lock or crash your system and at
// least you can compile and run the application on all versions of Windows;
// both 32 and 64 bit.
ShellExecute(Application.Handle, // HWND hwnd
'open', // LPCTSTR lpOperation
LPCTSTR(GetWindowsDir() + '\SysNative\OSK.EXE'), // LPCTSTR lpFile
'', // LPCTSTR lpParameters
'', // LPCTSTR lpDirectory,
SW_Show) ; // INT nShowCmd
}
It works well on 64 bit Windows 10. I haven't tried it with other versions yet, bit in theory, this should work with all versions of the OS, except 64-bit pre-Vista versions in which case the OSK won't show, but the 32-bit compiled application will run on all versions of Windows 32-bit and 64-bit.

Serial port enumeration in Delphi using SetupDiGetClassDevs

I'm trying to enumerate "friendly names" for COM ports. The ports may dynamically change as USB-serial devices are connected and disconnected at runtime.
Based on the possible methods described in this question, I am attempting to use the SetupDiGetClassDevs method.
I found this example code, but it is written for an older version of the setupapi unit (the original link to homepages.borland.com doesn't work of course).
I tried using the setupapi unit from the current JVCL(JVCL340CompleteJCL221-Build3845), but it doesn't seem to be compatible with Delphi 7. I get compiler errors:
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
#PropertyRegDataType,
#S1[1],RequiredSize,#RequiredSize) then begin
In the call to function SetupDiGetDeviceRegistryProperty,
I get the error "Types of actual and formal parameters must be identical" on the parameters #PropertyRegDataType, and #RequiredSize.
The Delphi3000 site says the code was written in 2004 and is intended for Delphi 7, so I'm not sure why it doesn't work with Delphi 7 now, unless setupapi has changed. Is anyone familiar with the changes to setupapi that could cause these problems?
I'm testing with a simple console program. The uses statement is " windows,
sysutils,
classes,
setupAPI,
Registry;"
The main program is:
begin
ComPortStringList := SetupEnumAvailableComPorts;
for Index := 0 to ComPortStringList.Count - 1 do
writeln(ComPortStringList[Index]);
end;
end.
The following procedure is working correctly for me (in Windows 8.1). It is important to use the parameter KEY_READ in the TRegistry.Constructor.
procedure EnumComPorts(const Ports: TStringList);
var
nInd: Integer;
begin { EnumComPorts }
with TRegistry.Create(KEY_READ) do
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKey('hardware\devicemap\serialcomm', False) then
try
Ports.BeginUpdate();
try
GetValueNames(Ports);
for nInd := Ports.Count - 1 downto 0 do
Ports.Strings[nInd] := ReadString(Ports.Strings[nInd]);
Ports.Sort()
finally
Ports.EndUpdate()
end { try-finally }
finally
CloseKey()
end { try-finally }
else
Ports.Clear()
finally
Free()
end { try-finally }
end { EnumComPorts };
I was able to get some more specific suggestions by asking the question a different way with different tags.
It turns out there were errors in the delphi3000.com example code, and possibly errors in the JVCL code. After fixing the example code errors, I got it to work. I have not addressed the potential JVCL errors.
Here is the working code (as a simple console app) for enumerating the names of com ports:
{$APPTYPE CONSOLE}
program EnumComPortsTest;
uses
windows,
sysutils,
classes,
setupAPI,
Registry;
{$R *.RES}
var
ComPortStringList : TStringList;
(*
The function below returns a list of available COM-ports
(not open by this or an other process), with friendly names. The list is formatted as follows:
COM1: = Communications Port (COM1)
COM5: = NI Serial Port (Com5)
COM6: = NI Serial Port (Com6)
COM7: = USB Serial Port (COM7)
COM8: = Bluetooth Communications Port (COM8)
COM9: = Bluetooth Communications Port (COM9)
This code originally posted at http://www.delphi3000.com/articles/article_4001.asp?SK=
errors have been fixed so it will work with Delphi 7 and SetupAPI from JVCL
*)
function SetupEnumAvailableComPorts:TstringList;
// Enumerates all serial communications ports that are available and ready to
// be used.
// For the setupapi unit see
// http://homepages.borland.com/jedi/cms/modules/apilib/visit.php?cid=4&lid=3
var
RequiredSize: Cardinal;
GUIDSize: DWORD;
Guid: TGUID;
DevInfoHandle: HDEVINFO;
DeviceInfoData: TSPDevInfoData;
MemberIndex: Cardinal;
PropertyRegDataType: DWord;
RegProperty: Cardinal;
RegTyp: Cardinal;
Key: Hkey;
Info: TRegKeyInfo;
S1,S2: string;
hc: THandle;
begin
Result:=Nil;
//If we cannot access the setupapi.dll then we return a nil pointer.
if not LoadsetupAPI then exit;
try
// get 'Ports' class guid from name
GUIDSize := 1; // missing from original code - need to tell function that the Guid structure contains a single GUID
if SetupDiClassGuidsFromName('Ports',#Guid,GUIDSize,RequiredSize) then begin
//get object handle of 'Ports' class to interate all devices
DevInfoHandle:=SetupDiGetClassDevs(#Guid,Nil,0,DIGCF_PRESENT);
if Cardinal(DevInfoHandle)<>Invalid_Handle_Value then begin
try
MemberIndex:=0;
result:=TStringList.Create;
//iterate device list
repeat
FillChar(DeviceInfoData,SizeOf(DeviceInfoData),0);
DeviceInfoData.cbSize:=SizeOf(DeviceInfoData);
//get device info that corresponds to the next memberindex
if Not SetupDiEnumDeviceInfo(DevInfoHandle,MemberIndex,DeviceInfoData) then
break;
//query friendly device name LIKE 'BlueTooth Communication Port (COM8)' etc
RegProperty:=SPDRP_FriendlyName;{SPDRP_Driver, SPDRP_SERVICE, SPDRP_ENUMERATOR_NAME,SPDRP_PHYSICAL_DEVICE_OBJECT_NAME,SPDRP_FRIENDLYNAME,}
SetupDiGetDeviceRegistryProperty(DevInfoHandle,
DeviceInfoData,
RegProperty,
PropertyRegDataType,
NIL,0,RequiredSize);
SetLength(S1,RequiredSize);
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
PropertyRegDataType,
#S1[1],RequiredSize,RequiredSize) then begin
KEY:=SetupDiOpenDevRegKey(DevInfoHandle,DeviceInfoData,DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ);
if key<>INValid_Handle_Value then begin
FillChar(Info, SizeOf(Info), 0);
//query the real port name from the registry value 'PortName'
if RegQueryInfoKey(Key, nil, nil, nil, #Info.NumSubKeys,#Info.MaxSubKeyLen, nil, #Info.NumValues, #Info.MaxValueLen,
#Info.MaxDataLen, nil, #Info.FileTime) = ERROR_SUCCESS then begin
RequiredSize:= Info.MaxValueLen + 1;
SetLength(S2,RequiredSize);
if RegQueryValueEx(KEY,'PortName',Nil,#Regtyp,#s2[1],#RequiredSize)=Error_Success then begin
If (Pos('COM',S2)=1) then begin
//Test if the device can be used
hc:=CreateFile(pchar('\\.\'+S2+#0),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0);
if hc<> INVALID_HANDLE_VALUE then begin
Result.Add(Strpas(PChar(S2))+': = '+StrPas(PChar(S1)));
CloseHandle(hc);
end;
end;
end;
end;
RegCloseKey(key);
end;
end;
Inc(MemberIndex);
until False;
//If we did not found any free com. port we return a NIL pointer.
if Result.Count=0 then begin
Result.Free;
Result:=NIL;
end
finally
SetupDiDestroyDeviceInfoList(DevInfoHandle);
end;
end;
end;
finally
UnloadSetupApi;
end;
end;
var
index : integer;
begin
ComPortStringList := SetupEnumAvailableComPorts;
if (ComPortStringList <> nil) and (ComPortStringList.Count > 0) then
for Index := 0 to ComPortStringList.Count - 1 do
writeln(ComPortStringList[Index]);
end.
Looks like some arguments of type PDWord were replaced by var DWord in SetupApi.pas. All you need is to remove '#' from these arguments in your code like that:
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
PropertyRegDataType,
#S1[1],RequiredSize,RequiredSize) then begin
Do you have "typed # operator" turned on? Project options, Compiler tab under "Syntax options". A lot of third party code breaks if that option is enabled.
For easier operation you might consider simply using the registry where those names are listed eg:
ErrCode := RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
'HARDWARE\DEVICEMAP\SERIALCOMM',
0,
KEY_READ,
KeyHandle);
(I've ommited the hand-waving stuff).
You might also consider using WMI - see this example from Magenta Systems - you can get a pretty much everything hardware-related now.
I adapted below code from RRUZ answer for Serial Port class. Works fine under Win10 20H2.
{$APPTYPE CONSOLE}
uses
SysUtils,
ActiveX,
ComObj,
Variants;
procedure GetWin32_SerialPortInfo;
const
WbemUser ='';
WbemPassword ='';
WbemComputer ='localhost';
wbemFlagForwardOnly = $00000020;
var
FSWbemLocator : OLEVariant;
FWMIService : OLEVariant;
FWbemObjectSet: OLEVariant;
FWbemObject : OLEVariant;
oEnum : IEnumvariant;
iValue : LongWord;
begin;
FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
FWMIService := FSWbemLocator.ConnectServer(WbemComputer, 'root\CIMV2', WbemUser, WbemPassword);
FWbemObjectSet:= FWMIService.ExecQuery('SELECT * FROM Win32_SerialPort','WQL',wbemFlagForwardOnly);
oEnum := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
while oEnum.Next(1, FWbemObject, iValue) = 0 do
begin
// for other fields: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-serialport
Writeln(Format('DeviceID %s',[String(FWbemObject.DeviceID)]));// String
Writeln(Format('Name %s',[String(FWbemObject.Name)]));// String
Writeln(Format('Description %s',[String(FWbemObject.Description)]));// String
FWbemObject:=Unassigned;
end;
end;
begin
try
CoInitialize(nil);
try
GetWin32_SerialPortInfo;
finally
CoUninitialize;
end;
except
on E:EOleException do
Writeln(Format('EOleException %s %x', [E.Message,E.ErrorCode]));
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
Output:
DeviceID COM7
Name Silicon Labs CP210x USB to UART Bridge (COM7)
Description Silicon Labs CP210x USB to UART Bridge
Press Enter to exit

Delphi - Using DeviceIoControl passing IOCTL_DISK_GET_LENGTH_INFO to get flash media physical size (Not Partition)

Alright this is the result of a couple of other questions. It appears I was doing something wrong with the suggestions and at this point have come up with an error when using the suggested API to get the media size. Those new to my problem I am working at the physical disk level, not within the confines of a partition or file system.
What I am doing
I am trying to get the size of a flash card from the first block to the last, boot record partition space and all. While I don't need it to dump the information from a card, I do want dynamic writing abilities. I would like to be able to say, put a marker so far from the end of a card, despite what size it might be. It was suggested that I pass IOCTL_DISK_GET_LENGTH_INFO to DeviceIoControl and at first I had no results but now I am finally getting an error from windows 50.
Source to the project
Here is the pastebin code for the main unit (Delphi 2009) - http://clutchx2.pastebin.com/iMnq8kSx
Here is the application source and executable with a form built to output the status of whats going on - http://www.mediafire.com/?js8e6ci8zrjq0de
Its probably easier to use the download, unless your just looking for problems within the code. I will also paste the code here.
unit Main;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TfrmMain = class(TForm)
edtDrive: TEdit;
lblDrive: TLabel;
btnMethod1: TButton;
btnMethod2: TButton;
lblSpace: TLabel;
edtSpace: TEdit;
lblFail: TLabel;
edtFail: TEdit;
lblError: TLabel;
edtError: TEdit;
procedure btnMethod1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TDiskExtent = record
DiskNumber: Cardinal;
StartingOffset: Int64;
ExtentLength: Int64;
end;
DISK_EXTENT = TDiskExtent;
PDiskExtent = ^TDiskExtent;
TVolumeDiskExtents = record
NumberOfDiskExtents: Cardinal;
Extents: array[0..0] of TDiskExtent;
end;
VOLUME_DISK_EXTENTS = TVolumeDiskExtents;
PVolumeDiskExtents = ^TVolumeDiskExtents;
var
frmMain: TfrmMain;
const
FILE_DEVICE_DISK = $00000007;
METHOD_BUFFERED = 0;
FILE_ANY_ACCESS = 0;
IOCTL_DISK_BASE = FILE_DEVICE_DISK;
IOCTL_VOLUME_BASE = DWORD('V');
IOCTL_DISK_GET_LENGTH_INFO = $80070017;
IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS = ((IOCTL_VOLUME_BASE shl 16) or (FILE_ANY_ACCESS shl 14) or (0 shl 2) or METHOD_BUFFERED);
implementation
{$R *.dfm}
function GetLD(Drive: Char): Cardinal;
var
Buffer : String;
begin
Buffer := Format('\\.\%s:',[Drive]);
Result := CreateFile(PChar(Buffer),GENERIC_READ Or GENERIC_WRITE,FILE_SHARE_READ,nil,OPEN_EXISTING,0,0);
If Result = INVALID_HANDLE_VALUE Then
begin
Result := CreateFile(PChar(Buffer),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,0,0);
end;
end;
function GetPD(Drive: Byte): Cardinal;
var
Buffer : String;
begin
If Drive = 0 Then
begin
Result := INVALID_HANDLE_VALUE;
Exit;
end;
Buffer := Format('\\.\PHYSICALDRIVE%d',[Drive]);
Result := CreateFile(PChar(Buffer),GENERIC_READ Or GENERIC_WRITE,FILE_SHARE_READ,nil,OPEN_EXISTING,0,0);
If Result = INVALID_HANDLE_VALUE Then
begin
Result := CreateFile(PChar(Buffer),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,0,0);
end;
end;
function GetPhysicalDiskNumber(Drive: Char): Byte;
var
LD : DWORD;
DiskExtents : PVolumeDiskExtents;
DiskExtent : TDiskExtent;
BytesReturned : Cardinal;
begin
Result := 0;
LD := GetLD(Drive);
If LD = INVALID_HANDLE_VALUE Then Exit;
Try
DiskExtents := AllocMem(Max_Path);
DeviceIOControl(LD,IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,nil,0,DiskExtents,Max_Path,BytesReturned,nil);
If DiskExtents^.NumberOfDiskExtents > 0 Then
begin
DiskExtent := DiskExtents^.Extents[0];
Result := DiskExtent.DiskNumber;
end;
Finally
CloseHandle(LD);
end;
end;
procedure TfrmMain.btnMethod1Click(Sender: TObject);
var
PD : DWORD;
CardSize: Int64;
BytesReturned: DWORD;
CallSuccess: Boolean;
begin
PD := GetPD(GetPhysicalDiskNumber(edtDrive.Text[1]));
If PD = INVALID_HANDLE_VALUE Then
Begin
ShowMessage('Invalid Physical Disk Handle');
Exit;
End;
CallSuccess := DeviceIoControl(PD, IOCTL_DISK_GET_LENGTH_INFO, nil, 0, #CardSize, SizeOf(CardSize), BytesReturned, nil);
if not CallSuccess then
begin
edtError.Text := IntToStr(GetLastError());
edtFail.Text := 'True';
end
else edtFail.Text := 'False';
CloseHandle(PD);
end;
end.
I placed a second method button on the form so I can write a different set of code into the app if I feel like it. Only minimal error handling and safeguards are there is nothing that wasn't necessary for debugging this via source.
Media Type & Interface
I tried this on a Sony Memory Stick using a PSP as the reader because I cant find the adapter for using a duo in my machine. The target is an MS and half of my users use a PSP for a reader half dont. However this should work fine on SD cards and that is a secondary target for my work as well. I tried this on a usb memory card reader and several SD cards.
The Problem
Now that I have fixed my attempt I get an error returned. 50 ERROR_NOT_SUPPORTED The request is not supported.
A similar application
I have found an application that uses this API as well as alot of related functions for what I am trying todo. I am getting ready to look into it the application is called DriveImage and its source is here - http://sourceforge.net/projects/diskimage/
The only thing I have really noticed from that application is there use of TFileStream and using that to get a handle on the physical disk.
I see what might be happening here.
Try this:
For every "FILE_SHARE_READ", change it to "FILE_SHARE_WRITE Or FILE_SHARE_READ"
From msdn:
"If you want to open a volume using \.\X:, you must use FILE_SHARE_WRITE | FILE_SHARE_READ, not just FILE_SHARE_WRITE. If you omit FILE_SHARE_READ, you'll get ERROR_NOT_SUPPORTED on most volumes"
http://msdn.microsoft.com/en-us/library/aa363858(v=vs.85).aspx
EDIT:
Your going to hate this, but the real reason it's failing is because your define for IOCTL_DISK_GET_LENGTH_INFO is wrong. Replace it with this:
((IOCTL_DISK_BASE shl 16) or (FILE_READ_ACCESS shl 14) or ($0017 shl 2) or METHOD_BUFFERED);
Which turns out to be 0x7405C not 0x80070017
The question is already answered. As an alternative you can use IOCTL_DISK_GET_DRIVE_GEOMETRY to obtain disk size. Here a working code that I used in my project:
const
IOCTL_DISK_GET_DRIVE_GEOMETRY = $00070000;
type
{$MINENUMSIZE 4}
TMediaType = (
Unknown, // Format is unknown
F5_1Pt2_512, // 5.25", 1.2MB, 512 bytes/sector
F3_1Pt44_512, // 3.5", 1.44MB, 512 bytes/sector
F3_2Pt88_512, // 3.5", 2.88MB, 512 bytes/sector
F3_20Pt8_512, // 3.5", 20.8MB, 512 bytes/sector
F3_720_512, // 3.5", 720KB, 512 bytes/sector
F5_360_512, // 5.25", 360KB, 512 bytes/sector
F5_320_512, // 5.25", 320KB, 512 bytes/sector
F5_320_1024, // 5.25", 320KB, 1024 bytes/sector
F5_180_512, // 5.25", 180KB, 512 bytes/sector
F5_160_512, // 5.25", 160KB, 512 bytes/sector
RemovableMedia, // Removable media other than floppy
FixedMedia, // Fixed hard disk media
F3_120M_512 // 3.5", 120M Floppy
);
{$MINENUMSIZE 1}
PDiskGeometry = ^TDiskGeometry;
TDiskGeometry = packed record
Cylinders: int64;
MediaType: TMediaType;
TracksPerCylinder: DWORD;
SectorsPerTrack: DWORD;
BytesPerSector: DWORD;
end;
var
H: THandle;
BytesReturned: DWORD;
DG: TDiskGeometry;
DSize: int64;
begin
H:= CreateFile(PChar('\\.\G:'), GENERIC_READ,
FILE_SHARE_WRITE or FILE_SHARE_READ, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if Handle = INVALID_HANDLE_VALUE then
raise Exception.Create('OOps!');
if not DeviceIOControl(H, IOCTL_DISK_GET_DRIVE_GEOMETRY, nil, 0,
#DG, SizeOf(TDiskGeometry), BytesReturned, nil) then
raise Exception.Create('OOps #2!');
DSize:= DG.Cylinders * DG.TracksPerCylinder;
DSize:= DSize * (DG.SectorsPerTrack * DG.BytesPerSector);
ShowMessage(IntToStr(DSize));
end;

Resources