Jump to content
Navid Madani

macOS process spawning: Exception class 6 when calling addObserver

Recommended Posts

unit uMacOSSpawn;

interface

uses
    System.SysUtils
  , System.Classes
  , Macapi.Foundation
  , Macapi.ObjectiveC
  , Macapi.ObjCRuntime
  , Macapi.Helpers
  ;

type
  TProcessSpawn = class(TOCLocal)
  private
    fTask: NSTask;
    fInputPipe: NSPipe;
    fOutputPipe: NSPipe;
    fErrorPipe: NSPipe;
    fNotify: TNotifyEvent;
    fOutput: string;
    function GetNotify: TNotifyEvent;
    procedure SetNotify(const Value: TNotifyEvent);
    function GetOutput: string;
  public
    constructor Create(FileName: PWideChar);
    destructor Destroy; override;
    procedure NotificationReceived(notification: NSNotification); cdecl;
    procedure Input(const aText: string);
    property Output: string read GetOutput;
    property Notify: TNotifyEvent read GetNotify write SetNotify;
  end;

implementation

{ TProcessSpawn }

constructor TProcessSpawn.Create(FileName: PWideChar);
var
  FileHandleLocObj: ILocalObject;
  InputLocObj: ILocalObject;
  OutputLocObj: ILocalObject;
  ErrorLocObj: ILocalObject;
begin
  inherited Create;

  fInputPipe  := TNSPipe.Create;
  fOutputPipe := TNSPipe.Create;
  fErrorPipe  := TNSPipe.Create;

  fTask := TNSTask.Wrap(TNSTask.Wrap(TNSTask.OCClass.alloc).init);
  fTask.setLaunchPath(StrToNSStr(FileName));

  if Supports(fInputPipe, ILocalObject, InputLocObj) and
     Supports(fOutputPipe, ILocalObject, OutputLocObj) and
     Supports(fErrorPipe, ILocalObject, ErrorLocObj) and
     Supports(fOutputPipe.fileHandleForReading, ILocalObject, FileHandleLocObj) then
  begin
    fTask.setStandardInput(InputLocObj.GetObjectID);
    fTask.setStandardOutput(OutputLocObj.GetObjectID);
    fTask.setStandardError(ErrorLocObj.GetObjectID);

    fOutputPipe.fileHandleForReading.acceptConnectionInBackgroundAndNotify;
    fOutputPipe.fileHandleForReading.readInBackgroundAndNotify;

    TNSNotificationCenter.Wrap(TNSNotificationCenter.OCClass.defaultCenter).addObserver(
      Self.GetObjectID,
      sel_getUid('NotificationReceived:'),
      NSFileHandleReadCompletionNotification,
      FileHandleLocObj.GetObjectID
    );

    fTask.launch;
    // fNSTask.waitUntilExit;
  end
  else
    raise Exception.Create('ILocalObject interface not supported.');
end;

destructor TProcessSpawn.Destroy;
begin
  fTask.release;
  fInputPipe.release;
  fOutputPipe.release;
  fErrorPipe.release;
  inherited;
end;

function TProcessSpawn.GetNotify: TNotifyEvent;
begin
  Result := fNotify;
end;

function TProcessSpawn.GetOutput: string;
begin
  Result := fOutput;
end;

procedure TProcessSpawn.NotificationReceived(notification: NSNotification);
var
  data: NSData;
  str: NSString;
  notifHandle: NSFileHandle;
begin
  notifHandle := TNSFileHandle.Wrap(notification.&object);
  if notifHandle = fOutputPipe.fileHandleForReading then
  begin
    data := fOutputPipe.fileHandleForReading.availableData;
    str := TNSString.Wrap(TNSString.Alloc.initWithData(data, NSUTF8StringEncoding));
    try
      fOutput := NSStrToStr(str);
      if Assigned(fNotify) then
        fNotify(Self);
    finally
      str.release;
    end;
  end;
end;

procedure TProcessSpawn.Input(const aText: string);
var
  data: NSData;
  str: NSString;
begin
  str := StrToNSStr(aText + #10);
  data := str.dataUsingEncoding(NSUTF8StringEncoding);
  fInputPipe.fileHandleForWriting.writeData(data);
end;

procedure TProcessSpawn.SetNotify(const Value: TNotifyEvent);
begin
  fNotify := Value;
end;

end.

I am trying to create a unit that spawns a console process on macOS that receives text input through stdin, and returns the result through stdout. I am not an iOS programmer, but I think I managed to get things done correctly based on documentation I consulted, and the unit below compiles without errors or warnings. However, the .addObserver call in the constructor (line 67) causes a runtime exception:

First chance exception at $00000001823C976C. Exception class 6. Process Project1 (2540).

Is this my fault, or could this be a bug in Delphi's Objective-C bridge?

Thanks to everyone in advance.

P.S.: I should also mention that debugging the RTL using Delphi 12 Athens on my Apple Silicon device running macOS Sonoma and Windows 11 Pro for ARM virtualized with Parallels, hangs the IDE 😞 

 

 

Edited by Navid Madani

Share this post


Link to post
2 hours ago, Navid Madani said:

However, the .addObserver call in the constructor (line 67) causes a runtime exception:

Assuming NSFileHandleReadCompletionNotification is an NSString, it should be passed like this: NSObjectToID(NSFileHandleReadCompletionNotification)

 

Having said that, I haven't come across the need to use such an observer. You might like to take a look at the code in Kastri that creates a process on macOS and reads from the output.

  • Thanks 1

Share this post


Link to post

Thanks, David. That was indeed what caused the exception. I noticed that pattern at least once in the docs I read and forgot.  The pattern above was based on Objective-C code samples I studied. However, mine does not work as intended because the notification handler is not called back. I did not know about Kastri until now.  I'll pursue the Kastri solution if tinkering with my own implementation remains fruitless. In any case, I just became a Kastri sponsor on GitHub. Thanks again.

  • Thanks 1

Share this post


Link to post
unit uMacOSSpawn;

interface

uses
    System.SysUtils
  , System.Classes
  , System.Threading
  , Macapi.Foundation
  , Macapi.ObjectiveC
  , Macapi.ObjCRuntime
  , Macapi.Helpers
  ;

type
  TStringCallback = procedure(const aText: string) of object;

  IProcessSpawn = interface
  ['{606A5174-5921-431C-AD57-DD037472CEF5}']
    function GetNotify: TStringCallback;
    procedure SetNotify(const Value: TStringCallback);
    function GetOutput: string;
    procedure Input(const aText: string);
    property Output: string read GetOutput;
    property OutputNotify: TStringCallback read GetNotify write SetNotify;
  end;

  TProcessSpawn = class(TInterfacedObject, IProcessSpawn)
  strict private
    fNSTask: NSTask;
    fInputPipe: NSPipe;
    fOutputPipe: NSPipe;
    fOutputNotify: TStringCallback;
    fOutput: string;
    fOutputTask: ITask;
    function GetNotify: TStringCallback;
    procedure SetNotify(const Value: TStringCallback);
    function GetOutput: string;
    procedure SendNotification(const aToNotify: TStringCallback; const aOutput: string);
    function WaitForOutput: ITask;
    procedure Input(const aText: string);
  public
    constructor Create(const FileName: string; const aCallback: TStringCallback);
    destructor Destroy; override;
  end;

implementation

{ TProcessSpawn }

constructor TProcessSpawn.Create(const FileName: string; const aCallback: TStringCallback);
var
  InputLocObj: ILocalObject;
  OutputLocObj: ILocalObject;
begin
  inherited Create;
  fOutputNotify := aCallback;
  fInputPipe  := TNSPipe.Create;
  fOutputPipe := TNSPipe.Create;
  fNSTask := TNSTask.Wrap(TNSTask.Wrap(TNSTask.OCClass.alloc).init);
  fNSTask.setLaunchPath(StrToNSStr(FileName));
  if Supports(fInputPipe, ILocalObject, InputLocObj) and
     Supports(fOutputPipe, ILocalObject, OutputLocObj) then
  begin
    fNSTask.setStandardInput(InputLocObj.GetObjectID);
    fNSTask.setStandardOutput(OutputLocObj.GetObjectID);
    fNSTask.setStandardError(OutputLocObj.GetObjectID);
    fNSTask.launch;
  end
  else
    raise Exception.Create('ILocalObject interface not supported.');
end;

destructor TProcessSpawn.Destroy;
begin
  if fNSTask.isRunning then
    fNSTask.terminate;
  fNSTask.release;
  fInputPipe.release;
  fOutputPipe.release;
  fOutputNotify := nil;
  fOutputTask.Cancel;
  fOutputTask := nil;
  inherited;
end;

function TProcessSpawn.GetNotify: TStringCallback;
begin
  Result := fOutputNotify;
end;

function TProcessSpawn.GetOutput: string;
begin
  Result := fOutput;
end;

procedure TProcessSpawn.Input(const aText: string);
var
  data: NSData;
  str: NSString;
begin
  if not fNSTask.isRunning then
    raise Exception.Create('No running process!');
  str := StrToNSStr(aText + #10);
  data := str.dataUsingEncoding(NSUTF8StringEncoding);
  fInputPipe.fileHandleForWriting.writeData(data);
  if fOutputTask = nil then
    fOutputTask := WaitForOutput;
end;

procedure TProcessSpawn.SendNotification(const aToNotify: TStringCallback;
                                       const aOutput: string);
begin
  fOutputTask := nil;
  if Assigned(aToNotify) then
  begin
    TThread.Synchronize(nil,
      procedure
      begin
        aToNotify(aOutput);
      end);
  end;
  fOutputTask := WaitForOutput;
end;

procedure TProcessSpawn.SetNotify(const Value: TStringCallback);
begin
  fOutputNotify := Value;
end;

function TProcessSpawn.WaitForOutput: ITask;
begin
  Result := TTask.Create(
    procedure
    var
      data: NSData;
      str: NSString;
    begin
      data := fOutputPipe.fileHandleForReading.availableData;
      str := TNSString.Wrap(TNSString.Alloc.initWithData(data, NSUTF8StringEncoding));
      try
        fOutput := NSStrToStr(str);
        SendNotification(fOutputNotify, fOutput);
      finally
        str.release;
      end;
    end);
  Result.Start;
end;

end.

In case anyone would run into this issue, I simplified things by calling .dataAvailable which is a blocking call in a separate thread. The unit below (at least for now) is working for my purposes.

 

 

Edited by Navid Madani
Added TStringCallback definition.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×