Recent

Author Topic: [SOLVED] Getting processes and owners  (Read 4572 times)

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
[SOLVED] Getting processes and owners
« on: August 21, 2020, 03:59:14 pm »
With a lot of help from users of this forum, I can now get a Windows service to detect user logons. However, for this feature to be any use I also need the user name. On investigation I have discovered that getting the user name from an environment variable doesn't work. Conventional methods will often give 'SYSTEM' or the host name followed by a $ because the service is not running as a user but under the system account. It seems a workaround is to get the owner of the process explorer.exe (the Windows Explorer), as this will always be running. This also isn't easy because it involves low-level programming.

I found the following code online (thanks to Rodrigo on https://theroadtodelphi.com/2011/11/06/wmi-tasks-using-delphi-%E2%80%93-processes/). This is unchanged apart from the count, which I added.
Code: Pascal  [Select][+][-]
  1. const
  2.   wbemFlagForwardOnly = $00000020;
  3. var
  4.    Count: Integer;
  5.    ErrorMsg: String;
  6.    FSWbemLocator : OLEVariant;
  7.    FWMIService   : OLEVariant;
  8.    FWbemObjectSet: OLEVariant;
  9.    FWbemObject   : OLEVariant;
  10.    oEnum         : IEnumvariant;
  11.    iValue        : LongWord;
  12.    NameOfUser    : OleVariant;
  13.    UserDomain    : OleVariant;
  14.  
  15. begin
  16.       // quick check parameters
  17.       ErrorMsg:=CheckOptions('h', 'help');
  18.       if ErrorMsg<>'' then begin
  19.             ShowException(Exception.Create(ErrorMsg));
  20.             Terminate;
  21.             Exit;
  22.       end;
  23.  
  24.       // parse parameters
  25.       if HasOption('h', 'help') then begin
  26.             WriteHelp;
  27.             Terminate;
  28.             Exit;
  29.       end;
  30.  
  31.       begin;
  32.         Count:= 0;
  33.         FSWbemLocator:= CreateOleObject('WbemScripting.SWbemLocator');
  34.         FWMIService:= FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', '');
  35.         FWbemObjectSet:= FWMIService.ExecQuery('SELECT * FROM Win32_Process', 'WQL', wbemFlagForwardOnly);
  36.         oEnum:= IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
  37.         while oEnum.Next(1, FWbemObject, iValue) = 0 do
  38.         begin
  39.           Inc(Count);
  40.           Write(IntToStr(Count) + ': ');
  41.           FWbemObject.GetOwner(NameOfUser, UserDomain);
  42.           Writeln(Format('Process  %s is owned by %s\%s',[String(FWbemObject.Name),String(NameOfUser), String(UserDomain)]));
  43.           FWbemObject:= Unassigned;
  44.         end;
  45.       end;
  46.       // stop program loop
  47.       Terminate;
  48. end;    
  49.  

It looks as if it should list all running processes and their owners. It's intended for Delphi but it compiled and ran in Lazarus as a console application for me but then failed immediately with the error
Code: Text  [Select][+][-]
  1. 1: Exception at 00000001000399F8: EVariantTypeCastError:
  2. Could not convert variant of type (Null) into type (String).

Of course, my understanding of objects is not good enough to fully get what this is doing (in particular, the types of the subvalues in the objects). I tried adding the condition "If Assigned(FWbemObject.Name)" then and then "If Assigned(FWbemObject)" then to the start of the Writeln but then the application wouldn't compile.

I have two questions:

1) Can anyone tell me what condition I need to add to avoid the error? and
2) Will the adapted code also work in a service, or is it again accessing processes not available there?

Thank you.
« Last Edit: August 26, 2020, 07:54:14 pm by TyneBridges »
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
Re: [SOLVED] Getting processes and owners
« Reply #1 on: August 21, 2020, 04:43:47 pm »
A bit more hunting revealed the answer. Adding the following early in the program avoided triggering the error:

Code: Pascal  [Select][+][-]
  1. NullStrictConvert:= False;

I'd still be interested to know whether the general approach should work in a service, although I guess I just need to try it and see...
« Last Edit: August 21, 2020, 04:59:38 pm by TyneBridges »
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

PascalDragon

  • Hero Member
  • *****
  • Posts: 5466
  • Compiler Developer
Re: Getting processes and owners
« Reply #2 on: August 21, 2020, 05:49:34 pm »
With a lot of help from users of this forum, I can now get a Windows service to detect user logons. However, for this feature to be any use I also need the user name. On investigation I have discovered that getting the user name from an environment variable doesn't work. Conventional methods will often give 'SYSTEM' or the host name followed by a $ because the service is not running as a user but under the system account. It seems a workaround is to get the owner of the process explorer.exe (the Windows Explorer), as this will always be running. This also isn't easy because it involves low-level programming.

From what I can see to retrieve the user name from the session ID you should be able to simply use WTSQuerySessionInformation with hServer set to WTS_CURRENT_SERVER_HANDLE. It's provided in FPC by unit JwaWtsApi32.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11446
  • FPC developer.
Re: Getting processes and owners
« Reply #3 on: August 22, 2020, 12:32:35 am »
1) Can anyone tell me what condition I need to add to avoid the error? and

Probably you need to use varisnul()

Jurassic Pork

  • Hero Member
  • *****
  • Posts: 1228
Re: Getting processes and owners
« Reply #4 on: August 22, 2020, 01:38:34 am »
hello,
As Marcov suggested, use varisnull function, like that for example :
Code: Pascal  [Select][+][-]
  1. if varisnull(NameOfUser) or  varisnull(UserDomain) then
  2.     Writeln(Format('Process  %s : owner not found',[String(FWbemObject.Name)]))
  3. else
  4.     Writeln(Format('Process  %s is owned by %s\%s',[String(FWbemObject.Name),String(NameOfUser), String(UserDomain)]));
  5.  
Friendly, J.P
« Last Edit: August 22, 2020, 01:40:30 am by Jurassic Pork »
Jurassic computer : Sinclair ZX81 - Zilog Z80A à 3,25 MHz - RAM 1 Ko - ROM 8 Ko

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11446
  • FPC developer.
Re: Getting processes and owners
« Reply #5 on: August 22, 2020, 01:56:33 pm »
Apropos, as result of an earlier WMI related question I made a small unit with an iterator for simple WMI queries.

it changes syntax like this:
Code: Pascal  [Select][+][-]
  1.  oEnum:= IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
  2.         while oEnum.Next(1, FWbemObject, iValue) = 0 do
  3. ...
  4.  

to

Code: Pascal  [Select][+][-]
  1. var
  2.   obj           : OLEVariant;
  3.   listiter      : oEnumIterator;
  4.   for obj in listiter.Enumerate(FWbemObjectset) do
  5.  

which beside neither saves on some variable declarations because they are bundled in the iterator record.

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
Re: Getting processes and owners
« Reply #6 on: August 23, 2020, 08:06:28 pm »
With a lot of help from users of this forum, I can now get a Windows service to detect user logons. However, for this feature to be any use I also need the user name. On investigation I have discovered that getting the user name from an environment variable doesn't work. Conventional methods will often give 'SYSTEM' or the host name followed by a $ because the service is not running as a user but under the system account. It seems a workaround is to get the owner of the process explorer.exe (the Windows Explorer), as this will always be running. This also isn't easy because it involves low-level programming.

From what I can see to retrieve the user name from the session ID you should be able to simply use WTSQuerySessionInformation with hServer set to WTS_CURRENT_SERVER_HANDLE. It's provided in FPC by unit JwaWtsApi32.

I've tried the following:
Code: Pascal  [Select][+][-]
  1. procedure TDaemon1.DataModuleControlCodeEvent(Sender: TCustomDaemon;
  2. ACode, AEventType: DWord; AEventData: Pointer; var Handled: Boolean);
  3. var evtype, sessionid: String;
  4.     DebugLog: TextFile;
  5.     GotIt: Boolean;
  6.     InfoClass: WTS_INFO_CLASS;
  7.     ResBytes: DWord;
  8.     Buf: Pointer;
  9.     SID: LongWord;
  10.  
  11. begin  // DataModuleControlEvent
  12.      // Remove 5 lines below and file declaration once this bit is debugged. The line below should ALWAYS be written if a user has logged on
  13.      NMsg:= 'Control event received ' + CT;
  14.      OurPath:= Copy(Paramstr(0), 1, LastPos('\', ParamStr(0)));
  15.      AssignFile(DebugLog, OurPath + 'debug.txt');
  16.      Rewrite(DebugLog);
  17.      Writeln(DebugLog, NMsg);
  18.      Flush(DebugLog);
  19.  
  20.      if ACode = SERVICE_CONTROL_SESSIONCHANGE then begin
  21.        case aEventType of
  22.        WTS_CONSOLE_CONNECT:
  23.             evtype:= 'Console Connect';
  24.        WTS_CONSOLE_DISCONNECT:
  25.             evtype:= 'Console Disconnect';
  26.        WTS_REMOTE_CONNECT:
  27.             evtype:= 'Remote Connect';
  28.        WTS_REMOTE_DISCONNECT:
  29.             evtype:= 'Remote Disconnect';
  30.        WTS_SESSION_LOGON:
  31.             evtype:= 'Session Logon';
  32.        WTS_SESSION_LOGOFF:
  33.             evtype:= 'Session Logoff';
  34.        WTS_SESSION_LOCK:
  35.             evtype:= 'Session Lock';
  36.        WTS_SESSION_UNLOCK:
  37.            evtype:= 'Session Unlock';
  38.        WTS_SESSION_REMOTE_CONTROL:
  39.             evtype:= 'Session Remote Control';
  40.        else
  41.             evtype:= Format('Unknown event type %d', [aEventType])
  42.        end;
  43.        if Assigned(aEventData) then
  44.        begin
  45.             SID:= PWTSSESSION_NOTIFICATION(aEventData)^.dwSessionId;
  46.             sessionid:= Format('%d', [PWTSSESSION_NOTIFICATION(aEventData)^.dwSessionId])
  47.        end else
  48.        begin
  49.             SID:= 0;
  50.             sessionid:= 'None';
  51.         end;
  52.  
  53.        NMsg:= Format('Session Change of type ''%s'' with session %s', [evtype, sessionid]);
  54.        GotIt:= WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, InfoClass, Buf, ResBytes);
  55.        If GotIt then NMsg:= NMsg + chr(13) + InfoClass.WTSUserName;
  56.        Writeln(DebugLog, NMsg);
  57.        CloseFile(DebugLog);
  58.     end;
  59. end;
  60.  

This fails to compile and gives the error identifier idents no member "WTSUserName" on line 55, although this seems to be the correct name for the subfield of the WTS_INFO_CLASS structure. I've probably missed something in the extraction of the data from the structure but can't find anything on line to clarify what to do.
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

PascalDragon

  • Hero Member
  • *****
  • Posts: 5466
  • Compiler Developer
Re: Getting processes and owners
« Reply #7 on: August 24, 2020, 02:07:45 pm »
WTS_INFO_CLASS is an enum, not a record. You need to pass the WTSUserName enum value to WTSQuerySessionInformation to tell it what to store into the pointer you provided. That pointer has to to be allocated by you.

Code: Pascal  [Select][+][-]
  1. var
  2.   // ...
  3.   username: array[0..100] of Char;
  4.   len: DWord;
  5.   // ...
  6. begin
  7.   // ...
  8.   len := Length(username);
  9.   GotIt := WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, @username[0], len);
  10.   if GotIt then
  11.     NMsg := NMsg + LineEnding + StrPas(@username);
  12.   // ...
  13. end;

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
Re: Getting processes and owners
« Reply #8 on: August 24, 2020, 04:58:45 pm »
WTS_INFO_CLASS is an enum, not a record. You need to pass the WTSUserName enum value to WTSQuerySessionInformation to tell it what to store into the pointer you provided. That pointer has to to be allocated by you.

Code: Pascal  [Select][+][-]
  1. var
  2.   // ...
  3.   username: array[0..100] of Char;
  4.   len: DWord;
  5.   // ...
  6. begin
  7.   // ...
  8.   len := Length(username);
  9.   GotIt := WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, @username[0], len);
  10.   if GotIt then
  11.     NMsg := NMsg + LineEnding + StrPas(@username);
  12.   // ...
  13. end;

Thanks for the clarification, but I added the missing variables and substituted your line for the call to WTSQuerySessionInformation and the compiler still didn't like the call. It gave me the error "Can't assign values to a variable" at position 100. That seems to be @username[0] (where it names the parameter as ppBuffer: pointer).
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

PascalDragon

  • Hero Member
  • *****
  • Posts: 5466
  • Compiler Developer
Re: Getting processes and owners
« Reply #9 on: August 25, 2020, 09:14:39 am »
Thanks for the clarification, but I added the missing variables and substituted your line for the call to WTSQuerySessionInformation and the compiler still didn't like the call. It gave me the error "Can't assign values to a variable" at position 100. That seems to be @username[0] (where it names the parameter as ppBuffer: pointer).

Ah, right, forgot that this is a var parameter. Try to add the following:

Code: Pascal  [Select][+][-]
  1. var
  2.   // ...
  3.   pusername: Pointer;
  4.   // ...
  5. begin
  6.   // ...
  7.   pusername := @username[0];
  8.   GotIt := WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, pusername, len);
  9.   if GotIt then
  10.     NMsg := NMsg + LineEnding + StrPas(@username); // yes, username, not pusername
  11.   // ...
  12. end;

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
Re: Getting processes and owners
« Reply #10 on: August 25, 2020, 03:31:08 pm »
Thanks for the clarification, but I added the missing variables and substituted your line for the call to WTSQuerySessionInformation and the compiler still didn't like the call. It gave me the error "Can't assign values to a variable" at position 100. That seems to be @username[0] (where it names the parameter as ppBuffer: pointer).

Ah, right, forgot that this is a var parameter. Try to add the following:

Code: Pascal  [Select][+][-]
  1. var
  2.   // ...
  3.   pusername: Pointer;
  4.   // ...
  5. begin
  6.   // ...
  7.   pusername := @username[0];
  8.   GotIt := WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, pusername, len);
  9.   if GotIt then
  10.     NMsg := NMsg + LineEnding + StrPas(@username); // yes, username, not pusername
  11.   // ...
  12. end;

It gets stranger. I amended my code as follows to help with debugging.
       
Code: Pascal  [Select][+][-]
  1.        NMsg:= Format('Session Change of type ''%s'' with session %s', [evtype, sessionid]);
  2.        pusername:= @username[0];
  3.        GotIt:= WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, pusername, len);
  4.        if GotIt then
  5.           NMsg:= NMsg + '. User is ' + StrPas(@username) // yes, username, not pusername
  6.        else
  7.            NMsg:= NMsg +  '. Session information query failed!';
  8.        Writeln(DebugLog, NMsg);
  9.        CloseFile(DebugLog);
  10.  

This time it compiled and installed OK. I expected either the desired result or the failed prompt but instead just got the following:
Code: Text  [Select][+][-]
  1. Control event received 25/8/20 14:20:26:461
  2. Session Change of type 'Session Logon' with session 23. User is
  3.  

This suggests that the function call succeeded but returned no value for the user name...  :(
« Last Edit: August 25, 2020, 03:37:06 pm by TyneBridges »
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

ASerge

  • Hero Member
  • *****
  • Posts: 2240
Re: Getting processes and owners
« Reply #11 on: August 25, 2020, 06:05:53 pm »
This suggests that the function call succeeded but returned no value for the user name...  :(
The WTSQuerySessionInformation function allocates memory for the user name itself. You only need to pass a pointer, and use it as a pointer! And of course, release the allocated memory at the end of use.
Code: Pascal  [Select][+][-]
  1. uses JwaWtsApi32;
  2.  
  3. procedure TForm1.Button1Click(Sender: TObject);
  4. var
  5.   UserName: Pointer;
  6.   Bytes: DWORD;
  7.   U: UnicodeString;
  8. begin
  9.   if WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, WTSUserName, UserName, Bytes) then
  10.   try
  11.     SetString(U, PUnicodeChar(UserName), (Bytes div SizeOf(UnicodeChar)) - 1); // Exclude last #0
  12.     Caption := UTF8Encode(U);
  13.   finally
  14.     WTSFreeMemory(UserName);
  15.   end;
  16. end;

PascalDragon

  • Hero Member
  • *****
  • Posts: 5466
  • Compiler Developer
Re: Getting processes and owners
« Reply #12 on: August 25, 2020, 09:17:07 pm »
This suggests that the function call succeeded but returned no value for the user name...  :(
The WTSQuerySessionInformation function allocates memory for the user name itself. You only need to pass a pointer, and use it as a pointer! And of course, release the allocated memory at the end of use.

Indeed. I've noticed that as well after I played around with the API myself now.

@TyneBridges: for your purposes you need to pass the session ID instead of WTS_CURRENT_SESSION.

TyneBridges

  • Full Member
  • ***
  • Posts: 150
    • Personal blog
Re: Getting processes and owners
« Reply #13 on: August 26, 2020, 04:51:30 pm »
This suggests that the function call succeeded but returned no value for the user name...  :(
The WTSQuerySessionInformation function allocates memory for the user name itself. You only need to pass a pointer, and use it as a pointer! And of course, release the allocated memory at the end of use.

Indeed. I've noticed that as well after I played around with the API myself now.

@TyneBridges: for your purposes you need to pass the session ID instead of WTS_CURRENT_SESSION.
Thank you PascalDragon and ASerge! With your help I've finally got the user name to display. For the information of anyone else struggling with this, here's one last code snippet with the code that worked for me. As it's running in a service, I changed the display code to write to my log file.
       
Code: Pascal  [Select][+][-]
  1.        NMsg:= Format('Session Change of type ''%s'' with session %s', [evtype, sessionid]);
  2.        GotIt:= WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, SID, WTSUserName, UserName, Bytes);
  3.        if GotIt then
  4.        begin
  5.          try
  6.             SetString(U, PUnicodeChar(UserName), (Bytes div SizeOf(UnicodeChar)) - 1); // Exclude last #0
  7.             NMsg:= NMsg + '. User is ' + UTF8Encode(U);
  8.          finally
  9.             WTSFreeMemory(UserName)
  10.          end; // Try
  11.        end else
  12.            NMsg:= NMsg +  '. Session information query failed!';
  13.        Writeln(DebugLog, NMsg);          
John H, north east England
Lover of the old Delphi, still inexperienced with FPC/Lazarus and not an instinctive programmer

 

TinyPortal © 2005-2018