Friday, 6 June 2025

Job Objects

- or How to bundle and control your (child) processes better.


This is partly a follow up to my old post on processes - found here, and an introduction to Job Objects.

A Job Object is a container where you can attach processes to, so that they are controlled within the same context.

To see what processes consist of Job Objects, we could install Process Explorer from Sysinternals from here

After installation Jobs by default are not shown, so you must select "Options|Configure Colors...", and enable the Jobs to be shown.

We can see that more and more applications tend to contain more and more processes and bundle these up - browsers and applications like Teams springs to mind.

The list of benefits of a Job Object is long:

- Allow a group of processes to be managed as a one.
- Starting and terminating related external processes.
- They can be named. 
- If main process dies, Windows will terminate the child processes.
- Can be nested.
- Accounting info – I/O, CPU usage
- More and more used – since more application consists of many processes
- They can impose limits on its processes.
- CPU: Maximum processes active, time, Affinity, rate control
- Memory: minimum and maximum working set, commit maximum
- Network: Maximum bandwidth 
- I/O: Maximum rate, read and write bytes 
- UI: User and GDI handles, clipboard access, exiting windows, desktop switching/creation

Below is an example of a Job with limitations:


Example

To illustrate a Job Object I want my "main" application to create a Job Object, create a Process and attach that to the Job Object - I want to pass a callback to get the output from the STDERR steam and if the "child" terminates I also want to be notified.

In the FormCreate I do create an instance of TChildThread - more on that later - with the parameters of the command of the process to run and the callback procdure:

var
  ChildExeName: string;
begin
  // Start Child process and attach to this process via Job Object
  ChildExeName := TPath.Combine(ExtractFilePath(ParamStr(0)), 'ChildProcessC.exe');
  if FileExists(ChildExeName) then
  begin
    ChildExeName := '"'+ChildExeName+'"';
    System.UniqueString(ChildExeName);
    ChildThread := TChildThread.Create(ChildExeName, LogChildError);
    ChildThread.OnTerminate := DidChildTerminateUnexpected;
  end
  else
    memLog.Lines.Add('Error: Not able to find any child process exe to start.');

The OnTerminate event is assigned to the procedure that notifies me of an unexpected termination.

And in the FormDestroy I terminated the child process and close its ProcessInfo handles. Remembering to set the onTerminate to nil prior - since this is the expected termination:

  if Assigned(ChildThread) and (ChildThread.ProcessInfo.hProcess>0) then
  begin
    ChildThread.OnTerminate := nil;
    TerminateProcess(ChildThread.ProcessInfo.hProcess, 0);
    ChildThread.Free;
  end;

Back to the TChildThread:

  TChildThread = class(TThread)
  const
    bufSize = 2400;
  private
    FReadBuf: array [0..bufSize] of AnsiChar;
    FCmd: string;
    FCallbackProc: TOnCaptureProc;
    FProcessInfo: TProcessInformation;
    function GethProcess: TProcessInformation;
    procedure SendLogMsg;
  public
    constructor Create(const cmd: string; CallBackProc: TOnCaptureProc);
    destructor Destroy; override;
    procedure Execute; override;
    property ProcessInfo: TProcessInformation read GethProcess;
  end;

and the callback is of type:

TOnCaptureProc = reference to procedure(const Value:string);

The most interesting thing here is the Execute procedure:

procedure TChildThread.Execute;
const
  secAttr: TSecurityAttributes = (
    nLength: SizeOf(TSecurityAttributes);
    bInheritHandle: True);
var
  rPipe: THandle; // Could PeekNamedPipe this for normal console info
  wPipe: THandle;
  erPipe: THandle; // STDERR pipe
  ewPipe: THandle;
  startupInfo: TStartupInfo;
  dRun, dAvail, dRead: DWORD;
  jobObject: NativeUInt;
  jobLimitInfo: TJobObjectExtendedLimitInformation;
begin
  inherited;
  if CreatePipe(rPipe, wPipe, @secAttr, 0) and 
     CreatePipe(erPipe, ewPipe, @secAttr, 0) then
  try
    FillChar(startupInfo, SizeOf(TStartupInfo), #0);
    startupInfo.cb := SizeOf(TStartupInfo);
    startupInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
    startupInfo.wShowWindow := SW_HIDE;
    startupInfo.hStdInput := rPipe;
    startupInfo.hStdOutput := wPipe;
    startupInfo.hStdError := ewPipe;

After having create the pipes to capture the STDERR (and STDIN and STDOUT), and set these in the StartupInfo structure, we create the Job Object and set some "Limits" for the Job Object.

    jobObject := CreateJobObject(nil, PChar(Format('Global\%d', [GetCurrentProcessID])));
    if jobObject <> 0 then
    begin
      jobLimitInfo.BasicLimitInformation.LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
      SetInformationJobObject(JobObject, JobObjectExtendedLimitInformation, @jobLimitInfo, SizeOf(TJobObjectExtendedLimitInformation));
    end;

Then we create the process and assign it to our Job Object

    if CreateProcess(nil, PChar(FCmd), @secAttr, @secAttr, True,   // !!!!
      CREATE_BREAKAWAY_FROM_JOB, nil, nil, startupInfo, FProcessInfo) then
    try
      if FProcessInfo.hProcess <> INVALID_HANDLE_VALUE then
        AssignProcessToJobObject(jobObject, FProcessInfo.hProcess);

And then we read from STDERR, and send any data back via the callback. 

      repeat
     dRun := WaitForSingleObject(FProcessInfo.hProcess, 100);
     PeekNamedPipe(erPipe, nil, 0, nil, @dAvail, nil);
     if (dAvail > 0) then
     repeat
       dRead := 0;
       ReadFile(erPipe, FReadBuf[0], bufSize, dRead, nil);
       FReadBuf[dRead] := #0;
       OemToCharA(FReadBuf, FReadBuf);
       Synchronize(SendLogMsg);
     until (dRead < bufSize);
   until (dRun <> WAIT_TIMEOUT);

And after termination, clean up and close the various handles.

    finally
      CloseHandle(FProcessInfo.hProcess);
      CloseHandle(FProcessInfo.hThread);
    end;
  finally
    CloseHandle(rPipe);
    CloseHandle(wPipe);
    CloseHandle(erPipe);
    CloseHandle(ewPipe);
  end;
end;

One important thing is to remember to set the Inheritable Handles to True - otherwise the handles to the "pipe" streams might not be what you expect - you want them typically to be the same as your "main" process.

Also note, that prior to Delphi 12, a needed Windows API const was missing, so you had to declare that yourself within your code:

const
  CREATE_BREAKAWAY_FROM_JOB       = $01000000;
  {$EXTERNALSYM CREATE_BREAKAWAY_FROM_JOB}

Documentation on Job Objects and of all the limit flag and other parameters found here: https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects

Update: There will be a session at CodeRage 2025 - will put the link here when a replay is avaiable.

/Enjoy




Monday, 24 February 2025

IAM Cloak, without the Dagger

 - or using a Keycloak setup as your Authentication solution in your native Delphi application.



With no reference to melodramatic intrigues, espionage, secret agents or a Marvel crime-fighting team involving experimental drugs, this post will just try to enlighten how fairly easy it is to get started with Keycloak as an authentication backend for your Delphi application.

This post is a continuation of some earlier post about IAM related stuff and my Auth component samples for PingOne, EntraID and now Keycloak on GitHub.

The posts can be found here:

GitHub gist/repos:
The EntraIDAuth was used in my Delphi Summit 2024 session: I can, therefore IAM.

Keycloak

Keycloak is a mature Open Source IAM solution, and I am by not means an expert - but I have been dabbling with some of the other provides - and this is as good as any.

The easiest way to get started is to install a docker container as described in the official documentation here.

There are other options and a lot more to it, but that short guide has you started, and we use that sample as the basis for the KeycloakAuth component, found in the last repo link above. Note that at the time of writing this post, it does only parse an access token for the "uniqueid", and the state parameter is by default added.

The state value is a GUID (or should I say UUID) - so to get "proper" UUID, I end up doing this:

if FUseState then
begin
  FStateValue := TGUID.NewGuid;
  URL := URL + '&state=' + GUIDToString(FStateValue).Trim(['{','}']).ToLower;
end;

Using the state parameter in the authorization code flow, helps to check that the client value sent matches the value in the response - to mitigate CSRF/XSRF (Cross-site request forgery) attacks. After that initial check one should then check the nonce given within the id_token.

But getting ahead of myself - to use and play around with my KeycloakAuth component - get it from GitHub as mentioned above - and install the component in the IDE (Delphi 12 package in repo).

As any of my other Auth components, this is based of TWebBrowser - and I hear you ask why not TEdgeBrowser? Well I had done some things similar years back before Edge was a thing, but since I do strongly suggest that the "SelectedEngine" property is set to "EdgeOnly" - TWebBrowser does internally use a TEdgeBrowser. It does mean you do need to install the Edge WebView2 SDK, and deploy the correct dll. Read all about it here.

Create a new VCL application and drop a KeycloakAuth component, align to client and set the following properties:

    RealmName = 'myrealm'
    AuthPath = 'http://localhost:8080/realms/'
    ClientId = 'myclient'
    RedirectUri = 'http://localhost:1234'
    AuthEndpoint = '/protocol/openid-connect/auth'
    TokenEndpoint = '/protocol/openid-connect/token'
    Scope = 'openid'
    UseState = False
    ResponseType = 'code'
    UserIdClaim = sub
    OnAuthenticated = KeycloakAuthenticated
    OnDenied = KeycloakDenied

Add an OnAuthenticated and OnDenied - like:

procedure TForm6.KeycloakAuthenticated(Sender: TObject);
begin
  ShowMessage('Welcome '+KeycloakAuth1.GreetName+'!'+
    sLineBreak+sLineBreak+'You have been authenticated as userId:'+
    KeycloakAuth1.UserId);
end;

procedure TForm6.KeycloakDenied(Sender: TObject);
begin
  ShowMessage('You have not been authenticated!');
end;

In the forms FormShow event call the components Authorize procedure, to start the flow.

I will add more capabilities to the component - such as nonce and code challenge, and cover all OpenID Connect response_type combinations.

To get a good grasp and overview of the OpenID Connect flows - take a look at this Medium post.

Happy 30th anniversary - Delphi! and see you at Delphi Summit 2025.

/Enjoy