Named Pipes from Windows Service to Client Application - delphi

My story is that I am designing a new app which must communicate with a Windows service. After much research I have come to the conclusion that Named Pipes are the recommended method ( How do I send a string from one instance of my Delphi program to another? ) however, it appears that I can't use SendMessage or Named Pipes in Win7 due to security problems... the messages never reach outside the service to the application.
I am using the Russell Libby's named Pipe components, which work without a hitch between normal desktop apps, but the Windows service seems to be throwing a wrench in the solution. Further research tells me that it may be possible to open up security on both sides to let them communicate, however, my knowledge level on this is minimal at best, and I haven't been able to make heads or tails of the possible API calls.
Based on the Delphi component pipes.pas, what needs to be done to open up this baby so both sides can start talking? I'm sure the following two functions from the pipes.pas file identify the security attributes, is anyone able to help me out here?
Thanks!
procedure InitializeSecurity(var SA: TSecurityAttributes);
var
sd: PSecurityDescriptor;
begin
// Allocate memory for the security descriptor
sd := AllocMem(SECURITY_DESCRIPTOR_MIN_LENGTH);
// Initialize the new security descriptor
if InitializeSecurityDescriptor(sd, SECURITY_DESCRIPTOR_REVISION) then
begin
// Add a NULL descriptor ACL to the security descriptor
if SetSecurityDescriptorDacl(sd, True, nil, False) then
begin
// Set up the security attributes structure
SA.nLength := SizeOf(TSecurityAttributes);
SA.lpSecurityDescriptor := sd;
SA.bInheritHandle := True;
end
else
// Failed to init the sec descriptor
RaiseWindowsError;
end
else
// Failed to init the sec descriptor
RaiseWindowsError;
end;
procedure FinalizeSecurity(var SA: TSecurityAttributes);
begin
// Release memory that was assigned to security descriptor
if Assigned(SA.lpSecurityDescriptor) then
begin
// Reource protection
try
// Free memory
FreeMem(SA.lpSecurityDescriptor);
finally
// Clear pointer
SA.lpSecurityDescriptor := nil;
end;
end;
end;

Windows Vista, Seven and 2008 enforce a more secure use of named pipes, see for example http://blogs.technet.com/b/nettracer/archive/2010/07/23/why-does-anonymous-pipe-access-fail-on-windows-vista-2008-windows-7-or-windows-2008-r2.aspx

When we migrated our product from Win 2K to Win7, we ran our Named Pipes quit working. After 2 weeks talking with MS (and $275), we discovered it was being caused by the Use Shared Folders file settings. Unchecking this feature allowed us to continue with pipes.

I tried to implement this one:
function GetUserSid(var SID: PSID; var Token: THandle): boolean;
var TokenUserSize: DWORD;
TokenUserP: PSIDAndAttributes;
begin
result := false;
if not OpenThreadToken(GetCurrentThread, TOKEN_QUERY, True, Token) then
if (GetLastError <> ERROR_NO_TOKEN) or
not OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token) then
Exit;
TokenUserP := nil;
TokenUserSize := 0;
try
if not GetTokenInformation(Token, TokenUser, nil, 0, TokenUserSize) and
(GetLastError <> ERROR_INSUFFICIENT_BUFFER) then
Exit;
TokenUserP := AllocMem(TokenUserSize);
if not GetTokenInformation(Token, TokenUser, TokenUserP,
TokenUserSize, TokenUserSize) then
Exit;
SID := TokenUserP^.Sid;
result := true;
finally
FreeMem(TokenUserP);
end;
end;
function ConvertSidToStringSidA(aSID: PSID; var aStr: PAnsiChar): BOOL; stdcall; external advapi32;
function ConvertStringSecurityDescriptorToSecurityDescriptorA(
StringSecurityDescriptor: PAnsiChar; StringSDRevision: DWORD;
SecurityDescriptor: pointer; SecurityDescriptorSize: Pointer): BOOL; stdcall; external advapi32;
const
SDDL_REVISION_1 = 1;
procedure InitializeSecurity(var SA: TSecurityAttributes; var SD; Client: boolean);
var OK: boolean;
Token: THandle;
pSidOwner: PSID;
pSid: PAnsiChar;
SACL: AnsiString;
begin
fillchar(SD,SECURITY_DESCRIPTOR_MIN_LENGTH,0);
// Initialize the new security descriptor
OK := false;
if InitializeSecurityDescriptor(#SD, SECURITY_DESCRIPTOR_REVISION) then begin
if Client or (OSVersionInfo.dwMajorVersion<6) then
// before Vista: add a NULL descriptor ACL to the security descriptor
OK := SetSecurityDescriptorDacl(#SD, true, nil, false)
else begin
// since Vista: need to specify special ACL
if GetUserSid(pSidOwner,Token) then
try
if ConvertSidToStringSidA(pSidOwner,pSid) then
try
SACL := 'D:(A;;GA;;;'+pSID+')(A;;GWGR;;;AN)(A;;GWGR;;;WD)S:(ML;;NW;;;S-1-16-0)';
OK := ConvertStringSecurityDescriptorToSecurityDescriptorA(
pointer(SACL),SDDL_REVISION_1,#SD,nil);
finally
LocalFree(PtrUInt(pSid));
end;
finally
FreeSid(pSidOwner);
CloseHandle(Token);
end;
end;
end;
if OK then begin
// Set up the security attributes structure
SA.nLength := sizeof(TSecurityAttributes);
SA.bInheritHandle := true;
SA.lpSecurityDescriptor := #SD;
end else
fillchar(SA,sizeof(SA),0); // mark error: no security
end;
It seems to work on the server side (i.e. the security attributes are created as expected), and you will have to write the client side code, without forgetting to add the pipe name in SYSTEM\CurrentControlSet\Services\lanmanserver\parameters\NullSessionPipes registry key, as expected.

I seem to remember that RemObjects has a named pipe client/server control in their package. Unless you are on a budget I would strongly recommend that you have a look at finished components for things like this. It is both time consuming and tricky to get right.
Alternatively, Justin Smyth has an article on named pipes right now. Check out his blog on the subject here: http://smythconsulting.blogspot.com/2011/07/smartmediaplayer-pipes-part4.html
Good luck!

I had the same kind of problem and just solved it. For me the reason it didn't work was because Russels TPipe implementetion has a check on the threads ID's just before the Pipe gets created: if not(Sync.SyncBaseTID = FNotifyThread) then..
It turned out I was creating the TPipeServer at the wrong place in my service. (I overrided DoStart etc instead of using the event OnStart... don't do that!)
I am now creating the TPipeServer instance in the same thread I later on activate it in.

Related

How to get the image path of all running processses as a standard user (XP / Server 2003 and below)?

I am trying to get the full path of all running processes in the system as a standard user without administrative rights on Windows XP / Server 2003. Getting the list of all running processes and handles to them is not a problem (Toolhelp "Process32First" / Native API "NtQuerySystemInformation" / PsApi "EnumProcesses"). My problem is that these calls do not return proper process handles, but some sort of handle that I first have to pass to "OpenProcess" to get a proper process handle that I then can use to query for the full image path (by calling "GetProcessImageFileName" or some low level function). But, for processes not started by the current user, "OpenProcess" fails if I am not an admin.
Can anybody point me in the rights direction on how to retrieve this information? Process Hacker and Process Explorer are able to do it, so it should be possible. I am aware that Process Hacker's source code is available, but as far as I understand it uses some sort of driver to query running processes.
Correction: As David Heffernan pointed out in his answer Process Explorer and Process Hacker do not display the full image path on Windows XP when started by a non admin user.
Here's the requested code (written in Delphi):
function GetProcessDetails (const th32ProcessID: THandle) : String;
var
szImageFileName : array [0..MAX_PATH] of Char;
hProcess : THandle;
begin
hProcess := OpenProcess (PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
false, th32ProcessID);
if (hProcess = 0) then
exit;
if (GetProcessImageFileName (hProcess, #szImageFileName [0],
MAX_PATH) > 0) then
Result := szImageFileName;
CloseHandle (hProcess);
end; { GetProcessDetails }
And here's the function that uses "Process32First / Process32Next" to retrieve the process information:
procedure FillProcessListToolHelp;
var
hSnapShot : THandle;
PE : TProcessEntry32;
sImageFileName : String;
begin
hSnapShot := CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0);
PE.dwSize := SizeOf (TProcessEntry32);
if (Process32First (hSnapShot, PE)) then
repeat
if (PE.th32ProcessID <> 0) then
sImageFileName := GetProcessDetails (PE.th32ProcessID);
until (Process32Next (hSnapShot, PE) = false);
CloseHandle (hSnapShot);
end; { FillProcessListToolHelp }
Please be aware that "SeDebugPrivilege" has been assigned before making the call to "FillProcessListToolHelp". Also, right now I am only interested in a solution for 32 bit Windows.
The code that you present works as intended and is the best that you can do. You can add a little more diagnostics to it to see better what is going on:
hProcess := OpenProcess (PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
false, th32ProcessID);
if (hProcess = 0) then
begin
Writeln(IntToStr(GetLastError));
exit;
end;
What you will then learn is that when OpenProcess is failing, it is because GetLastError is returning 5, aka ERROR_ACCESS_DENIED. And that is because, as you yourself have identified, the processes in question are owned by a different user.
Windows security means that your standard user process is simply unable to open a handle to these processes with the necessary access rights. If you wish to obtain information about these processes you will need to execute your code with sufficient privileges, for example by running as administrator.
I believe that the best you can do without admin privileges is to use the information returned in the szExeFile member of TProcessEntry32.
{$APPTYPE CONSOLE}
uses
System.SysUtils, Winapi.Windows, Winapi.TlHelp32;
function GetProcessImageFileName(hProcess: THandle; lpImageFileName: LPTSTR;
nSize: DWORD): DWORD; stdcall;
external 'PSAPI.dll' name 'GetProcessImageFileNameW';
function ImageFileName(const PE: TProcessEntry32): string;
var
szImageFileName: array [0 .. MAX_PATH] of Char;
hProcess: THandle;
begin
Result := PE.szExeFile; // fallback in case the other API calls fail
hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false,
PE.th32ProcessID);
if (hProcess = 0) then
exit;
if (GetProcessImageFileName(hProcess, #szImageFileName[0], MAX_PATH) > 0) then
Result := szImageFileName;
CloseHandle(hProcess);
end;
procedure FillProcessListToolHelp;
var
hSnapShot: THandle;
PE: TProcessEntry32;
begin
hSnapShot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PE.dwSize := SizeOf(TProcessEntry32);
if (Process32First(hSnapShot, PE)) then
repeat
if (PE.th32ProcessID <> 0) then
Writeln(ImageFileName(PE));
until (Process32Next(hSnapShot, PE) = false);
CloseHandle(hSnapShot);
end;
begin
FillProcessListToolHelp;
Readln;
end.
Now, in the question you state:
Process Hacker and Process Explorer are able to do it, so it should be possible.
But that is not the case. At least for Process Explorer which is what I use. Here's what Process Explorer, when running as standard user, has to offer for the smss process:
Having just tried out Process Hacker, I can see that it will yield the information you want, even when not running elevated. So if you wish to do this yourself you simply need to read the Process Hacker and do what it does.

WM_COPYDATA string not appearing in target application

I'm trying to pass information between two of my applications in Delphi 2010.
I'm using a simplified version of code that I've used successfully in the past (simplified because I don't need the sender to know that the send has been successful) I've boiled down the send received to a pair of example applications, which in essence are as follows
Send
procedure TMF.SendString;
var
copyDataStruct: TCopyDataStruct;
s: AnsiString;
begin
s := ebFirm.Text;
copyDataStruct.cbData := 1 + length(s);
copyDataStruct.lpData := PAnsiChar(s);
SendData(copyDataStruct);
end;
procedure TMF.SendData(copyDataStruct: TCopyDataStruct);
var
rh: THandle;
res: integer;
begin
rh := FindWindow(PChar('TMF'), PChar('Get Phone'));
if rh = 0 then
begin
// Launch the target application
ShellExecute(Handle, 'open', GetPhone, nil, nil, SW_SHOWNORMAL);
// Give time for the application to launch
Sleep(3000);
SendData(copyDataStruct); // RECURSION!
end;
SendMessage(rh, WM_COPYDATA, Integer(Handle), Integer(#copyDataStruct));
end;
Receive Application
procedure TMF.WMCopyData(var Msg: TWMCopyData);
var
s : AnsiString;
begin
s := PAnsiChar(Msg.CopyDataStruct.lpData) ;
jobstatus.Panels[1].Text := s;
end;
The major difference between the working test applications and the application I am adding the code to is that there is a lot of extra activity going on in target application. Especially on startup.
Any suggestions on why the WMCopyData procedure seems not to be firing at all?
CHeers
Dan
There are a few problems with your code.
One, you are not assigning a unique ID to the message. The VCL, and various third-party components, also use WM_COPYDATA, so you have to make sure you are actually processing YOUR message and not SOMEONE ELSE'S message.
Two, you may not be waiting long enough for the second app to start. Instead of Sleep(), use ShellExecuteEx() with the SEE_MASK_WAITFORINPUTIDLE flag (or use CreateProcess() and WaitForInputIdle()).
Third, when starting the second app, your recursive logic is attempting to send the message a second time. If that happened to fail, you would launch a third app, and so on. You should take out the recursion altogether, you don't need it.
Try this:
var
GetPhoneMsg: DWORD = 0;
procedure TMF.SendString;
var
copyDataStruct: TCopyDataStruct;
s: AnsiString;
begin
if GetPhoneMsg = 0 then Exit;
s := ebFirm.Text;
copyDataStruct.dwData := GetPhoneMsg;
copyDataStruct.cbData := Length(s);
copyDataStruct.lpData := PAnsiChar(s);
SendData(copyDataStruct);
end;
procedure TMF.SendData(copyDataStruct: TCopyDataStruct);
var
rh: HWND;
si: TShellExecuteInfo;
res: Integer;
begin
rh := FindWindow(PChar('TMF'), PChar('Get Phone'));
if rh = 0 then
begin
// Launch the target application and give time to start
ZeroMemory(#si, SizeOf(si));
si.cbSize := SizeOf(si);
si.fMask := SEE_MASK_WAITFORINPUTIDLE;
si.hwnd := Handle;
si.lpVerb := 'open';
si.lpFile := GetPhone;
si.nShow := SW_SHOWNORMAL;
if not ShellExecuteEx(#si) then Exit;
rh := FindWindow(PChar('TMF'), PChar('Get Phone'));
if rh = 0 then Exit;
end;
SendMessage(rh, WM_COPYDATA, WParam(Handle), LParam(#copyDataStruct));
end;
initialization
GetPhoneMsg := RegisterWindowMessage('TMF_GetPhone');
Receive Application
var
GetPhoneMsg: DWORD = 0;
procedure TMF.WMCopyData(var Msg: TWMCopyData);
var
s : AnsiString;
begin
if (GetPhoneMsg <> 0) and (Msg.CopyDataStruct.dwData = GetPhoneMsg) then
begin
SetString(s, PAnsiChar(Msg.CopyDataStruct.lpData), Msg.CopyDataStruct.cbData);
jobstatus.Panels[1].Text := s;
end else
inherited;
end;
initialization
GetPhoneMsg := RegisterWindowMessage('TMF_GetPhone');
I think it is a good habit to add
copyDataStruct.dwData := Handle;
in procedure TMF.SendString; - if you don't have a custom identifier, putting the source HWND value will help debugging on the destination (you can check for this value in the other side, and therefore avoid misunderstand of broadcasted WMCOPY_DATA e.g. - yes, there should not be, but I've seen some!).
And
procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA;
in TMF client class definition, right?
There should be a missing exit or else after the nested SendData call:
procedure TMF.SendData(copyDataStruct: TCopyDataStruct);
(...)
Sleep(3000);
SendData(copyDataStruct);
end else
SendMessage(rh, WM_COPYDATA, NativeInt(Handle), NativeInt(#copyDataStruct));
end;
But this won't change much.
Check the rh := FindWindow() returned handle: is it the Handle of the TMF client form, or the Application.Handle?
It doesn't work anymore if you are using Windows 7.
If you are using it, check this page to see how to add an exception: http://msdn.microsoft.com/en-us/library/ms649011%28v=vs.85%29.aspx
I thought there was a problem with the (rh) handle being 0 when you call it, if the app needed to be started. But now I see that SendData calls itself recursively. I added a comment in the code for that, as it was non-obvious. But now there's another problem. The 2nd instance of SendData will have the right handle. But then you're going to pop out of that, back into the first instance where the handle is still 0, and then you WILL call SendMessage again, this time with a 0 handle. This probably is not the cause of your trouble, but it's unintended, unnecessary, and altogether bad. IMO, this is a case complicating things by trying to be too clever.

Delphi Get the handle of a EXE

Heres an example of how I'm doing it right now :
var
Client : String;
Handle : Integer;
begin
Client := 'Window Name';
GetWindowThreadProcessId(FindWindow(nil, PAnsiChar(Client)), #ProcessId);
Handle := OpenProcess(PROCESS_ALL_ACCESS, False, ProcessId);
end;
I'd rather grab the Process's handle with its exe name...
Is this possible this?
Since the link provided by vcldeveloper is broken, here's the full function code that works without 3rd party components.
First we will find process id (PID), and then we'll get process handle by opening all access (since the OP mentioned in the comments he will need this for ReadProcessMemory functionality).
If the function for PID returns 0, it means that the process is most likely not running (or just not found in the running process list)
function GetPIDbyProcessName(processName:String):integer;
var
GotProcess: Boolean;
tempHandle: tHandle;
procE: tProcessEntry32;
begin
tempHandle:=CreateToolHelp32SnapShot(TH32CS_SNAPALL, 0);
procE.dwSize:=SizeOf(procE);
GotProcess:=Process32First(tempHandle, procE);
{$B-}
if GotProcess and not SameText(procE.szExeFile, processName) then
repeat GotProcess := Process32Next(tempHandle, procE);
until (not GotProcess) or SameText(procE.szExeFile,processName);
{$B+}
if GotProcess then
result := procE.th32ProcessID
else
result := 0; // process not found in running process list
CloseHandle(tempHandle);
end;
Next, we will get/open Process handle from the PID we got. The whole code/usage is as follows:
var myPID, myProcessHandle: integer;
begin
myPID:=GetPIDbyProcessName('someExeName.exe');
myProcessHandle:=OpenProcess(PROCESS_ALL_ACCESS,False,myPID);
end;
You should store the myProcessHandle in such way that it is accessable for ReadProcessMemory(myProcessHandle...) as first parameter.
Also, add these to your global uses clauses:
Winapi.Windows (for ReadProcessMemory and OpenProcess)
Winapi.tlHelp32(for getting PID tProcessEntry32 variable)
Application.Handle?
Looks like you're trying to program in Delphi by WinAPI. In the vast majority do not need it, VCL provides appropriate object-oriented wrappers.
May be You will find something in this component pack: GLibWMI
You can use ProcessInfo:
var
ProcessInfo : TProcessInfo;
Process : TProcessItem;
PID: Cardinal;
ProcessHandle : THandle;
begin
ProcessInfo := TProcessInfo.Create(nil);
try
Process := ProcessInfo.RunningProcesses.FindByName('Notepad.exe');
if Assigned(Process) then
begin
PID := Process.ProcessID;
ProcessHandle := OpenProcess(PROCESS_ALL_ACCESS,False,PID);
if ProcessHandle > 0 then
try
{Add your code here}
finally
CloseHandle(ProcessHandle);
end;
end;
finally
ProcessInfo.Free;
end;
end;
If you do not like using a third-party component, you can study source code of ProcessInfo to see how it retrieves list of running processes, and their properties. Basically it relays on Windows Tool Help API for most of its features.

how to store settings in resource

i am trying to store some settings in resource of my application
but failed
i dont want to use ini file or registry methods
i am using this code
var
data :string;
procedure WriteSettings(ServerFile: string; Settings: string);
var
ResourceHandle: THandle;
pwServerFile: PWideChar;
begin
GetMem(pwServerFile, (Length(ServerFile) + 1) * 2);
try
StringToWideChar(ServerFile, pwServerFile, Length(ServerFile) * 2);
ResourceHandle := BeginUpdateResourceW(pwServerFile, False);
UpdateResourceW(ResourceHandle, MakeIntResourceW(10), 'SETTINGS', 0, #Settings[1], Length(Settings) + 1);
EndUpdateResourceW(ResourceHandle, False);
finally
FreeMem(pwServerFile);
end;
end;
function ReadSettings(ServerFile: string): string;
var
ServerModule: HMODULE;
ResourceLocation: HRSRC;
ResourceSize: dword;
ResourceHandle: THandle;
ResourcePointer: pointer;
begin
ServerModule := LoadLibrary(pchar(ServerFile));
try
ResourceLocation := FindResource(ServerModule, 'SETTINGS', RT_RCDATA);
ResourceSize := SizeofResource(ServerModule, ResourceLocation);
ResourceHandle := LoadResource(ServerModule, ResourceLocation);
ResourcePointer := LockResource(ResourceHandle);
if ResourcePointer <> nil then
begin
SetLength(Result, ResourceSize - 1);
CopyMemory(#Result[1], ResourcePointer, ResourceSize);
FreeResource(ResourceHandle);
end;
finally
FreeLibrary(ServerModule);
end;
end;
procedure TForm1.saveClick(Sender: TObject);
begin
writesettings(paramastr(0),'true');
end;
procedure TForm1.ReadClick(Sender: TObject);
begin
data:=readsettings(paramstr(0));
end;
begin
if data='true' then checkbox1.checked:=true;
end
but is nit storing the that i wrote to resource :(
is there any other better options?
any help please
The documentation for BeginUpdateResource clearly states why your code doesn't work (emphasis added):
pFileName [in]
LPCTSTR
The binary file in which to update resources. An application must be able to obtain write-access to this file; the file referenced by pFileName cannot be currently executing. If pFileName does not specify a full path, the system searches for the file in the current directory.
You might have been able to deduce the cause of the error yourself if you were checking the API function's return value and calling GetLastError on failure, like the documentation advises.
You can store settings in a resource, but you can't store settings in a resource of the program whose settings you're trying to store. And now that we've established that you're not allowed to store settings in the program itself, you may as well just abandon the resource idea and use a more conventional method of storing settings in an external location, such as the registry, an INI file, or whatever. You might still wish to read a set of default settings from a resource if you find that the external location doesn't yet have any settings, as might happen after a fresh install.
Having your program modify itself is a bad idea. As a couple people already pointed out, this will fail badly under Vista and Win7 in most cases. It's better not to fight the operating system. Windows already provides a couple different ways for your program to store its settings. You can drop an INI or other config file in some folder outside of Program Files, or you can store it in the Registry, which is probably the best option.

Detect if an OCX class is registered in Windows

i need to know how can detect if an OCX class (ClassID) is registred in Windows
something like
function IsClassRegistered(ClassID:string):boolean;
begin
//the magic goes here
end;
begin
if IsClassRegistered('{26313B07-4199-450B-8342-305BCB7C217F}') then
// do the work
end;
you can check the existence of the CLSID under the HKEY_CLASSES_ROOT in the windows registry.
check this sample
function ExistClassID(const ClassID :string): Boolean;
var
Reg: TRegistry;
begin
try
Reg := TRegistry.Create;
try
Reg.RootKey := HKEY_CLASSES_ROOT;
Result := Reg.KeyExists(Format('CLSID\%s',[ClassID]));
finally
Reg.Free;
end;
except
Result := False;
end;
end;
The problem with (many, many) suggestions of crawling the registry is that:
there is more than one registry location you would need to look at
a class can be registered and not exist in the registry
Registration-free COM allows a class to be available without it being registered. Conceptually you don't want to know if a class is "registered", you just want to know it is registered enough to be created.
Unfortunately the only (and best) way to do that is to create it:
//Code released into public domain. No attribution required.
function IsClassRegistered(const ClassID: TGUID): Boolean;
var
unk: IUnknown;
hr: HRESULT;
begin
hr := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or CLSCTX_LOCAL_SERVER, IUnknown, {out}unk);
unk := nil;
Result := (hr <> REGDB_E_CLASSNOTREG);
end;
ActiveX/COM is a complex beast, registrations have many pieces to them, and Vista+ onward make it more complicated with UAC Registry Virtualization rules.
The best option is to simply attempt to instantiate the OCX and see if it succeeds or fails. That will tell you whether the OCX is registered correctly, all the pieces are hooked up, whether the OCX is even usable within the calling user's context, etc.

Resources