What's on my network?

Dear Anyone,
I have had this up on the network page for 6 weeks, with no response, and as it is causing me considerable grief, I am putting it up here too.

I want to find out what resources are on a network.

WNetOpenEnumW and WNetEnumResourceW, (which I used for years successfully) only work with SMB1 enabled. As this is not the default on W11, it falls over. SMB1 is being actively discouraged, so this is not the way to go.

There is obviously some way to do it, as windows explorer does it most successfully.

I have had this problem up on stack overflow for the same time, again with zero responders.

Please, is there somebody out there who can help me?

Chester Wilson

Perhaps you could do what you need using WMI?

VB.NET example, but wmi is quite doable in delphil

MS usually has multiple overlapped sets of API’s to do the same thing, albeit often with different bugs/restrictions. Have you tried NetShareEnum et al?

ADSI has similar functionality too.

Alex

I did some digging into the WNetOpenEnum/SMB1 issue, as I have one utility possibly still in circulation using these functions.
I don’t have in depth network knowledge, it’s an immense field and I’ve always gotten by learning what I needed to know and studying code samples, but hopefully the following will help somewhat.

From what I understand, the functions aren’t obsolete but they do rely on SMB. Version 1 was in use for a long time, before v2 and 3 were introduced, but v1 remained the default as older devices (NAS, etc.) couldn’t be updated.

So the problem with these functions now comes in 2 flavours:

  • newer devices cannot access older devices that can only use SMB1
  • newer devices cannot access older devices where SMB1 is still the default (Win7, 2003, etc.)

In the first case the only solutions are to override the default and reinstate SMB1 in the calling machine (lots of posts about this), or use ADS (see below).

In the latter case , disabling SMB1 fixes the problem (good article here

and a very interesting discussion here

regarding trouble doing it sometimes, and how to overcome - “best answer” halfway down - by the poster, actually.

In terms of code, I found that without SMB1 all that can be easily achieved is getting local shares, and the names of other machines on the network (but not the folders they share on the network - which requires elevated access rights and other complications, unlike NetOpenEnum et al).

There are several ways to get local shares with WMI, and here’s one way to list other machines on a network, using ADS

uses ActiveDs_TLB, adshlp;

procedure findComputers(domain:string; Alist:TStrings);
var
ADsTempObj,
vFilter: OleVariant;
Enum: IEnumVariant;
Value: Cardinal;
DomCont: IADsContainer;
Filter: WideString;
ADs: IADs;
tmp: string;
begin
OleCheck(ADsGetObject(StringToOleStr(‘WinNT://’+UpperCase(domain)), IADsContainer, DomCont));
Filter := ‘Computer’;
VariantInit(vFilter);
OleCheck(ADsBuildVarArrayStr(@Filter, 1, variant(vFilter)));
DomCont.Set_Filter(vFilter);
Enum := (Domcont._NewEnum) as IEnumVariant;
while Enum.Next(1, ADsTempObj, Value) = S_OK do
begin
if Value = 0 then
Break;
ADs := IUnknown(ADsTempObj) as IADs;
Alist.Add(ADs.Name);
end;
end;

ex: findComputers(‘WORKGROUP’,Memo1.Lines); (for some reason I couldn’t pass a variable (stringlist, string etc…), only a component)
(needs ActiveDs toolbar activated, and adshlp.pas (happy to supply, not easy to find)

derived from code found here

Did you actually instantiate the TStringList object before passing it it?

If not then that would be why, the Memo1.Lines internal TStringList is already instantiated.

Sadly, not that simple…
I have found the solution, though. After trying everything (local/global var, build up the entries into a simple string, off-loading each entry to a local string to pass it to the list, etc.), I remembered catching an odd comment in a code sample while looking into this:

“// Causes Access Violation if AD query does not happen in subroutine”

and sure enough, passing the query to another routine before calling the ADS makes the call behave normally, with any variable.

However further tests show that a tricky maze of conditions still needs to be navigated to avoid access violations altogether:
→ The ADS querying routine must be called from a sub-routine function (not a procedure),
→ There are then 2 ways to collect the results from the sub-routine
1- the sub-routine can pass a global variable to the ADS routine, this global variable can then be read by any subsequent procedure,
2- the sub-routine can be called by a procedure passing it a local variable, AS LONG AS that variable is not referred to again in the sub-routine after the ADS call

I suspect the explanation for these strange intricacies is above my pay grade but if anyone has one (or any indication there’s a simpler way) I’d love to hear it…

So here’s the (improved) ADS calling function, and 2 ways to call it (other than a straight call passing the results to a component):

procedure findComputers(domain:string; Alist:TStrings);
var
ADsTempObj,
vFilter: OleVariant;
Enum: IEnumVariant;
Value: Cardinal;
DomCont: IADsContainer;
Filter: WideString;
ADs: IADs;
begin
try
CoInitialize(nil);
OleCheck(ADsGetObject(StringToOleStr(‘WinNT://’+UpperCase(domain)), IADsContainer, DomCont));
Filter := ‘Computer’;
VariantInit(vFilter);
OleCheck(ADsBuildVarArrayStr(@Filter, 1, variant(vFilter)));
DomCont.Set_Filter(vFilter);
Enum := (Domcont._NewEnum) as IEnumVariant;
while Enum.Next(1, ADsTempObj, Value) = S_OK do
begin
if Value = 0 then
Break;
ADs := IUnknown(ADsTempObj) as IADs;
Alist.Add(ADs.Name);
ADs._Release;
end;
finally
CoUninitialize;
end;
end;

--------------option 1---------------
var
GlobalList: TStringList.

function call_FindComputers_option_1(domain: string): boolean;
begin
result:= false;
if GlobalList = nil then
exit;
findComputers(domain,GlobalList);
result:= GlobalList.Count > 0; //OK to manipulate result
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
GlobalList:= TstringList.Create;
call_FindComputers_option_1(‘WORKGROUP’);
Memo1.Lines:= GlobalList;
GlobalList.Free;
end;

--------------option 2---------------

function call_FindComputers_option_2(domain: string; Rslt: TStrings): boolean;
begin
findComputers(domain,Rslt);
//result:= Rslt.Count > 0; // this will trigger an access violation
end;

procedure TForm1.Button1Click(Sender: TObject);
var
LocalList: TstringList;
begin
LocalList:= TstringList.Create;
call_FindComputers(‘WORKGROUP’,LocalList); //causes access violation if ADS query called from a procedure
Memo1.Lines:= LocalList;
LocalList.Free;
end;

My guess is the definition of AD functions is different from expected. I had to do some manipulation of your code just to get it to compile using the JEDI ADS library. After I got it to compile I have not been able to get it to crash.

function ADsGetObject(lpszPathName: LPCWSTR; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall;
function ADsBuildVarArrayStr(lppPathNames: LPWSTR; dwPathNames: DWORD;
  var pVar: OleVariant): HRESULT; stdcall;
uses JwaAdsHlp, ...;

...

procedure TForm4.findComputers(domain:string; Alist:TStrings);
var
  ADsTempObj,
  vFilter: OleVariant;
  Enum: IEnumVariant;
  Value: Cardinal;
  DomCont: IADsContainer;
  pDomCont: Pointer;
  Filter: WideString;
  ADs: IADs;
begin
  try
    CoInitialize(nil);
    OleCheck(ADsGetObject(StringToOleStr('WinNT://'+UpperCase(domain)), IADsContainer, pDomCont));
    Filter := 'Computer';
    VariantInit(vFilter);
    DomCont := IADsContainer(pDomCont);
    OleCheck(ADsBuildVarArrayStr(@Filter, 1, vFilter));
    DomCont.Set_Filter(vFilter);
    Enum := (Domcont._NewEnum) as IEnumVariant;
    while Enum.Next(1, ADsTempObj, Value) = S_OK do
    begin
      if Value = 0 then
        Break;
      ADs := IUnknown(ADsTempObj) as IADs;
      Alist.Add(ADs.Name);
      ADs._Release;
    end;
  finally
    CoUninitialize;
  end;
end;

I’ve been curious to have a play around with this but have only just gotten around to doing it.

Created a new project using Geoff’s example below and while it runs without an error, at most it only finds the computer that it is running on.

I currently have 3 computers running on my local Lan which is not set up as a domain so I am passing the Workgroup name to the findComputers procedure.

On 2 of the computers it finds just itself and on the third it returns an empty list.

When you were running this Geoff (or others), did it give you a list of other computers on your Lan?

I suspect the results you get may vary depending on whether network discovery is enabled on the machines.

On windows 11

Control Panel\All Control Panel Items\Network and Sharing Centre\Advanced sharing settings

Just speculating, I haven’t run the code and my pc is on a separate netwokr from the other pc’s at home (for safety!), so wouldn’t find anything anyway.

To be honest I don’t actually have a specific need to be able to find other computers on the Lan but it would be nice to know how to do it in the same way that Windows can.

All of my systems are running the latest build of Windows 10 and have network discovery turned on.

Right now I’m running 4 systems. 3 Physical and one VM. When I open Windows Explorer it is showing all 4 systems but the findComputers procedure doesn’t.

This has always been a bit of Black Magic within Windows. I’m really curious now to how to achieve this from code. :blush:

1 Like

Ciuly’s web corner has a link to setting up Delphi to be able to use WMI. Unfortunately this link is broken, and a web search merely returns you to the page with the broken link. Any ideas?

?

WMI’s interface in Delphi should already be present in the Library (WbemScripting_TLB.pas).
You will also need to install Active DS (ActiveDs_TLB.pas):
Component > Import Component… > Import a Type Libray > Active DS
You will also need adshlp.pas (attached).

Just add these to your “uses” to work with WMI functionality.

Hope this helps,

Didier

adshlp.pas (5.92 KB)

You could try putting the link in to Internet Archive: Wayback Machine and see if it has been saved…