macOS SCNetworkReachability API

From Lazarus wiki
Jump to navigationJump to search
macOSlogo.png

This article applies to macOS only.

See also: Multiplatform Programming Guide

English (en)



Overview

The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host. A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.

The SCNetworkReachability programming interface supports a synchronous and an asynchronous model. In the synchronous model, you get the reachability status by calling the SCNetworkReachabilityGetFlags function. In the asynchronous model, you can schedule the SCNetworkReachability object on the run loop of a client object’s thread. The client implements a callback function to receive notifications when the reachability status of a given remote host changes. Note that these functions follow Core Foundation naming conventions. A function that has "Create" or "Copy" in its name returns a reference you must release with the CFRelease function.

The example below implements the asynchronous model which is useful for monitoring connectivity.

Example code

{
Pascal GUI version of the Apple sample command line utility SimpleReach in C
which demonstrates the System Configuration framework reachability APIs.
https://developer.apple.com/library/archive/samplecode/SimpleReach/Introduction
/Intro.html#//apple_ref/doc/uid/DTS10003238
}

unit Unit1;

{$mode objfpc}{$H+}
{$linkframework SystemConfiguration}

interface

uses
  SysUtils, Forms, StdCtrls, MacOSAll, Controls;

type

  { TForm1 }

  TForm1 = class(TForm)
    MonitorButton: TButton;
    QuitButton: TButton;
    HostnameEdit: TEdit;
    HostnameLabel: TLabel;
    Memo1: TMemo;
    procedure FormKeyPress(Sender: TObject; var Key: char);
    procedure MonitorButtonClick(Sender: TObject);
    procedure QuitButtonClick(Sender: TObject);
  private

  public

  end;

  // pocedure requires mwpascal calling convention
  type TProcedure = procedure(target: SCNetworkReachabilityRef;
    flags: SCNetworkConnectionFlags; info: pointer); mwpascal;

var
  Form1: TForm1;
  callout: TProcedure;
  monitoring: Boolean = False;

implementation

{$R *.lfm}

{ TForm1 }

// Print flag meanings
procedure print_header;
begin
    Form1.Memo1.Lines.Add('T' + #09 + 'kSCNetworkFlagsTransientConnection');
    Form1.Memo1.Lines.Add('R' + #09 + 'kSCNetworkFlagsReachable');
    Form1.Memo1.Lines.Add('C' + #09 + 'kSCNetworkFlagsConnectionRequired');
    Form1.Memo1.Lines.Add('A' + #09 + 'kSCNetworkFlagsConnectionAutomatic');
    Form1.Memo1.Lines.Add('I' + #09 + 'kSCNetworkFlagsInterventionRequired');
    Form1.Memo1.Lines.Add('L' + #09 + 'kSCNetworkFlagsIsLocalAddress');
    Form1.Memo1.Lines.Add('D' + #09 + 'kSCNetworkFlagsIsDirect' + LineEnding);
end;

// The procedure to print current reachability flags
procedure print_flags(flags: SCNetworkConnectionFlags);
var
  statusStr: String;
begin
  statusStr := DateTimeToStr(Now) + ' : ';

  // The specified node name or address can be reached via a transient
  //  connection, such as PPP.
  if(flags and kSCNetworkFlagsTransientConnection > 0) then
    statusStr := statusStr + 'T'
  else
    statusStr := statusStr + '-';

  // The specified node name or address can be reached using the current network
  // configuration.
  if(flags and kSCNetworkFlagsReachable > 0) then
    statusStr := statusStr + 'R'
  else
    statusStr := statusStr + '-';

  // The specified node name or address can be reached using the current network
  // configuration, but a connection must first be established.
  // eg Returned for a dialup connection that was not currently active
  if(flags and kSCNetworkFlagsConnectionRequired > 0) then
    statusStr := statusStr + 'C'
  else
    statusStr := statusStr + '-';

  // The specified node name or address can be reached using the current network
  // configuration, but a connection must first be established.
  // Any traffic directed to the specified name or address will initiate the
  // connection
  if(flags and kSCNetworkFlagsConnectionAutomatic > 0) then
    statusStr := statusStr + 'A'
  else
    statusStr := statusStr + '-';

  // The specified node name or address can be reached using the current network
  // configuration, but a connection must first be established.
  // Returned when there is a dial-on-traffic configuration and manual
  // intervention required eg due to no dial tone, password etc.
  if(flags and kSCNetworkFlagsInterventionRequired > 0) then
    statusStr := statusStr + 'I'
  else
    statusStr := statusStr + '-';

  // The specified node name or address is one associated with a network
  // interface on the current system.
  if(flags and kSCNetworkFlagsIsLocalAddress > 0) then
    statusStr := statusStr + 'L'
  else
    statusStr := statusStr + '-';

  // Network traffic to the specified node name or address does not go through
  // a gateway, but is routed directly to one of the interfaces in the system.
  if(flags and kSCNetworkFlagsIsDirect > 0) then
    statusStr := statusStr + 'D'
  else
    statusStr := statusStr + '-';

  Form1.Memo1.Lines.Add(statusStr);
end;

// The procedure to be called when the reachability of the target changes.
procedure status_callback(target: SCNetworkReachabilityRef; flags: SCNetworkConnectionFlags; info: pointer); mwpascal;
begin
  print_flags(flags);
end;

// Start monitoring/ Stop monitoring
procedure TForm1.MonitorButtonClick(Sender: TObject);
var
  context: SCNetworkReachabilityContext;
  hostname: PChar;
  charArray: Array[0..254] of Char;
  count: Integer;
  status: boolean;
  target: SCNetworkReachabilityRef;
begin
  // Action iff we have text in HostnameEdit.Text
  if(length(HostnameEdit.Text) = 0) then
    exit;

  // Update monitoring status
  if(monitoring = True) then
    begin
      // Remove our callback from the runloop.
      SCNetworkReachabilityUnscheduleFromRunLoop(target, CFRunLoopGetCurrent, kCFRunLoopDefaultMode);
      // Update status of button, memo, flag
      MonitorButton.Caption := 'Monitor';
      Form1.Memo1.Lines.Add('Monitoring stopped');
      monitoring := False;
      exit;
    end;

  // Set flag
  monitoring := True;
  MonitorButton.Caption := 'Stop';

  // Print flag meanings
  print_header;

  // Convert HostnameEdit.Text to PChar; ensure final char is null
  hostname := charArray;
  for count := 0 to 254 do
     hostname[count] := #00;
  for count := 0 to Length(HostnameEdit.Text) do
     hostname[count] := HostnameEdit.Text[count + 1];

  // Setup the context structure for the callback - we just use the hostname
  context.copyDescription := Nil; // Nil if unused
  context.info    := hostname;    // C pointer to user-specified block of data
  context.release := Nil;         // Nil if unused
  context.retain  := Nil;         // Nil if unused
  context.version := 0;           // Structure version always zero

  // Setup our callback procedure variable to be called when the reachability of
  // the target changes.
  callout := @status_callback;

  Try
    // Create the target from the hostname
    target := SCNetworkReachabilityCreateWithName(Nil, hostname);

    // Set our callback
    status := SCNetworkReachabilitySetCallback(target, callout, context);
    if(status <> true) then
      Form1.Memo1.Lines.Add('callback status err: ' + BoolToStr(status));

    // Install our callback on the runloop.
    status := SCNetworkReachabilityScheduleWithRunLoop(target, CFRunLoopGetCurrent, kCFRunLoopDefaultMode);
    if(status <> true) then
      Form1.Memo1.Lines.Add('schedule status err: ' + BoolToStr(status));
  Finally
    // Housekeeping
    CFRelease(target);
  End;
end;

// Allow ENTER key to trigger Monitor Button
// Form1.KeyPreview must be set to True in code/Obj Inspector
procedure TForm1.FormKeyPress(Sender: TObject; var Key: char);
begin
  if(key = #13) then
    MonitorButton.Click;
end;

// Exit application
procedure TForm1.QuitButtonClick(Sender: TObject);
begin
  if(monitoring = True) then
    begin
      MonitorButton.Click;
      Application.ProcessMessages;
      Sleep(750);
    end;

  Close;
end;

end.

External links