Tuesday, September 11, 2012

Using Named Pipes in Linux

                                Using Named Pipes in Linux
Overview

A main feature of my Exploratory Project was the use of MS Pipes to communicate between user applications and then, when added, with the display application.  WinSockets were also used but mostly MS Pipes since easier and I wanted to stay away from exposing the applications to the Internet; but used enough so that I could communicate with my apps when running on a different PC.

Therefore, to move the Exploratory Project to Linux I was going to need to use something to communicate between applications.  Eventually I'll implement sockets.  For first I wanted to implement named pipes; those created by mkfifo.

Using a google result that purported to show how to do it, I gave it a try.  But it
didn't completely work for me.  It created the pipe file descriptor pair that I shall
refer to as the pipe handle and could open the read-only handle in a secondary thread
of the test application but the application process would hang when I attempted to open the write-only handle via the terse instructions of the internet post.  So I gave it up and went onto getting other op sys interfaces working.

In the last few of days I went back to it and found that if I just tried to open the
second handle as read-write rather than write-only that it worked.  I could now send
messages from the main process thread to one of the created threads.  Since in the
Exploratory Project the pipes are used between applications, I created a second test
application to read the named pipe rather than a different thread of the first test
application.

After that immediately worked I decided to do full-duplex that I would need for the
Exploratory Project.  Thinking of what I had learned using Microsoft Pipes, I created
an Ada package to allow multiple applications to communicate with each other, one pair of applications per full-duplex pipe.

First Results

Since, at first, the full mechanism needed for the Exploratory Project had yet to be
implemented, I had the first application create the named pipe via mkfifo and then
opens it for read-write and write three short messages.  In the meantime the second
application was started in a second terminal window.  It spun on its read-only open
of the pipe until the open succeeded and then does a read of enough bytes to more than read everything written by the first test.  This read all three messages written by the first test in one read request and returned the number of bytes read.

So all is well if the application that does the mkfifo is launched first.  If the
second application is launched first, it waits for a valid open and then attempts the
read.  But although the first application does its writes soon after the mkfifo (it does
create and then start its two created threads three times before writing to the pipe),
the second application never returns from its attempt to read the pipe.  So something
yet to look into in that regard.

Full-Duplex Results

However, I then extended the named pipe Ada package into the Full Duplex Pipe package where two named pipes with related names are opened instead of one.  This, of course, results in four file descriptors - two for each named pipe with one for read and one for write.  The names have a common path with a common leading name string and a suffix that identifies the pair of applications such as "01to02" and "02to01" for applications one (test1) and two (test2).

In this implementation, both pipe pairs are symmetric.  That is, both applications
attempt to create/make the pipe pairs and, when done, to unlink them from the Linux
operating system.  Also, both fileClient writes
to its pipe and the Server reads that pipe.  The Server writes the response to its pipe
and the Client application reads it. 

So test1 names itself (i.e., 1) as the Client and 2, for test2, as the Server.  This
results in a pipe name suffix of 01to02 for the Client and 02to01 for the Server.  For
test2, it is the reverse so when it reads from its Server pipe it is reading from
01to02 and the same pipe that test1 is writing to as its Client pipe.  (Two digits are
used since 99 applications should be way more than enough - in fact I have limited the
range from 1 to 15.)

The test1 application calls the open and then, in a loop, writes to its Client pipe
and reads from its Server pipe.  The test2 application also calls the open and then,
in a loop, reads its Server pipe and responds on its Client pipe.  In this test, after
three iterations both applications close the pair of pipes and exit.  All worked
immediately.

full_duplex_pipe

A new Ada package was created to contain the named pipe (Linux fifo pipe) procedures for full-duplex communications.  The code is as follows.

Note:  More text output to the terminal window was included to check that the test
was running as expected.  That output has been removed in the code below since not
really part of the use of full-duplex named pipes.


with Apps;

generic
-- ++
--| Overview:
--|   Generic package to access, when instantiated, to either read or write
--|   the particular, instantiated named pipe as the pipe client or the pipe
--|   server.  The use, by an application, of either the client instantiation
--|   or the server instantiation and the use, by a different application, of
--|   the reverse instantiation, will result in a full duplex pipe with the
--|   pair of applications being able to communicate with each other when
--|   both running under Linux on the same PC.
--|
--|   At instantiation specify a number for each of a pair of applications;
--|   the current application as the Client and the other application as the
--|   Server.  If the other application does the same it will also name itself
--|   as the Client and the first application as the Server.
--|
--|   When, for instance, application 1 needs to send a request to application
--|   2, it will have a named pipe called "NamedPipe01to02" as the Client pipe
--|   and will use it to transmit to application 2 and application 2 will
--|   have the same pipe as its Server pipe and use it to receive from
--|   application 1.  While application 2 will have a Client named pipe named
--|   "NamedPipe02to01" to use transmit to application 1 and application 1 will
--|   have the same pipe to use to receive from application 2.
-- --

  --| Notes:
  --|   Parameters to supply when instantiate an instance of a topic.

  Client_App
  --| Number of the application instantiating this package
  : Apps.Application_Number_Type;

  Server_App
  --| Number of the application to which to communicate
  : Apps.Application_Number_Type;

package Full_Duplex_Pipe is

  subtype Message_Length_Type
  is Integer range 0..1000; -- assuming max length message is 1000

  subtype Message_Data_Type
  is String( 1..Message_Length_Type'last );

  type Message_Type
  is record
    Count : Message_Length_Type;
    --| Number of bytes in message
    Data  : Message_Data_Type;
    --| Message data from 1 to Count
  end record;

  procedure Close;
  --| Close any opened named pipe pair.
  --| Notes:
  --|   Each named pipe of the pair has two file descriptors; one for
  --|   receive and one for transmit.

  procedure Open
  ( Wait : in Boolean := True
    --| True if to wait/delay for data to read
  );
  --| Open the named pipe pair.
  --| Notes:
  --|   o Any particular instantiation of this package can only open for
  --|     one direction for each of the Client and the Server pipes.
  --|   o The Server pipe will be opened to Receive and the Client pipe
  --|     will be opened to Transmit.
  --|   o An open for transmit will also create the named pipe prior to
  --|     the open.

  ------------------------------------------------------------------------------

  package Client is
  --| Overview:
  --|   The pipe used by the currently running application to send
  --|   request messages to the other application of an application
  --|   pair.  That is, Client-->Server.  For instance, using pipe
  --|   "NamedPipe01to02" where the other application (that is, 2)
  --|   will receive using the same pipe.

    procedure Transmit
    ( Message : in Message_Type
      --| Request message to be written to the pipe
    );
    --| Write the Message to the opened named pipe

  end Client;

  ------------------------------------------------------------------------------

  package Server is
  --| Overview:
  --|   The pipe used by the currently running application to receive
  --|   request messages from the other application of an application
  --|   pair.  That is, Client-->Server.  For instance, using pipe
  --|   "NamedPipe01to02" where the other application (that is, 1) will
  --|   transmit using this pipe and this application when it is 2 will
  --|   receive the requests of application 1.

    procedure Receive
    ( Message : out Message_Type
      --| Message read from the pipe
    );
    --| Read the contents of the opened named pipe

  end Server;

end Full_Duplex_Pipe;

The package body:

with Console;
with Exec_Itf;
with Numeric_Conversion;
with Unchecked_Conversion;

package body Full_Duplex_Pipe is

  type Pipe_Selector_Type
  is ( Client_Pipe,
       Server_Pipe );

  type Pipe_Info_Type
  is record
    Name   : Exec_Itf.Named_Pipe_Name_Type;
    --| Pipe Name with its App suffix and with trailing ASCII.NUL added for C
    Opened : Boolean;
    --| Whether pipe has been opened
    Handle
    --| Handle (file descriptor) of named pipe
    : Exec_Itf.Pipe_Handle;
  end record;

  type Pipe_Info_Array_Type
  is array ( Pipe_Selector_Type ) of Pipe_Info_Type;

  Pipe
  --| Data about the pipe pair
  : Pipe_Info_Array_Type;

  ------------------------------------------------------------------------------

  package body Client is

    procedure Transmit
    ( Message : in Message_Type
    ) is

      Bytes_Written
      --| Number of bytes actually written
      : Integer;

    begin -- Transmit

      if Pipe(Client_Pipe).Opened then
        Bytes_Written := Exec_Itf.Write_File
                         ( File => Pipe(Client_Pipe).Handle,
                           Addr => Message.Data'address,
                           Len  => Message.Count );
      end if;

    end Transmit;

  end Client;

  ------------------------------------------------------------------------------

  package body Server is

    --| Notes:
    --|   Static data.

    Msg_Buffer
    --| Buffer to read into for response from Server app
    --| Notes: Used to allow receive to read more data than caller wants.
    : Message_Data_Type := ( others => ASCII.NUL );

    procedure Receive
    ( Message : out Message_Type
    ) is

      Bytes_Read
      --| Number of bytes read
      : Integer;

    begin -- Receive

      if Pipe(Server_Pipe).Opened then
        Bytes_Read := Exec_Itf.Read_File( File => Pipe(Server_Pipe).Handle,
                                          Addr => Msg_Buffer'address,
                                          Num  => Message_Length_Type'last );
        Exec_Itf.Log_Error;
        if Bytes_Read > 0 and then
           Bytes_Read <= Message_Length_Type'last -- greater should never occur
        then
          Message.Data(1..Bytes_Read) := Msg_Buffer(1..Bytes_Read);
          Message.Count := Bytes_Read;
        end if;
      end if;

    end Receive;

  end Server;

  ------------------------------------------------------------------------------

  procedure Close is
  -- ++
  --| Logic_Flow:
  --|   Close both pipes of the pair if thought to be open.
  --| Notes:
  --|   The other application may have closed the pipes first.
  -- --

    Result
    : Integer;

    Status
    --| Function return status
    : Boolean;

  begin -- Close

    if Pipe(Client_Pipe).Opened then
      Status := Exec_Itf.Close_File( Handle => Pipe(Client_Pipe).Handle );
      Exec_Itf.Log_Error;
      -- Remove the FIFO pipe
      Result := Exec_Itf.Unlink( Pipe(Client_Pipe).Name.Path'address );
      Exec_Itf.Log_Error;
      Pipe(Client_Pipe).Opened := False;
    end if;

    if Pipe(Server_Pipe).Opened then
      Status := Exec_Itf.Close_File( Handle => Pipe(Server_Pipe).Handle );
      Exec_Itf.Log_Error;
      -- Remove the FIFO pipe
      Result := Exec_Itf.Unlink( Pipe(Server_Pipe).Name.Path'address );
      Exec_Itf.Log_Error;
      Pipe(Server_Pipe).Opened := False;
    end if;

  end Close;

  ------------------------------------------------------------------------------

  procedure Open
  ( Wait : in Boolean := True
  ) is
  -- ++
  --| Logic_Flow:
  --|   Open both the client and server pipes.
  --| Notes:
  --|   o The client pipe is to be opened for transmit to the server and
  --|     the server pipe is to be opened for receive.
  --|   o There will be an attempt to create/make each pipe before the open
  --|     since don't know which application will be running first.
  --|   o Therefore each will be opened for read/write so it won't hang
  --|     the application.
  --|   o But only one of the pipe pair will be written to with the other
  --|     read by one application and the reverse by the other application.
  --|     Therefore, app 1 (for instance) will write to pipe 01to02 and app2
  --|     will read from pipe 01to02; that is, one will write to the write
  --|     fifo pipe and the other read from it.  For full duplex the roles
  --|     are reversed so that the other fifo pipe of the pair is used.
  -- --

    Open_Mode
    : Exec_Itf.Open_Mode_Type;

    Result
    : Integer;

    Suffix1
    : Numeric_Conversion.String_Type;

    Suffix2
    : Numeric_Conversion.String_Type;

    function to_Int is new Unchecked_Conversion
                           ( Source => Exec_Itf.Pipe_Handle,
                             Target => Integer );

    use type Exec_Itf.Open_Mode_Type;

  begin -- Open

    Suffix1 := Numeric_Conversion.Integer_to_Ascii
               ( Number => Client_App,
                 Count  => 2,    -- two digits in 01..15
                 Retain => True, -- retain leading 0's
                 Signed => False );
    Suffix2 := Numeric_Conversion.Integer_to_Ascii
               ( Number => Server_App,
                 Count  => 2, -- two digits in 01..15
                 Retain => True,
                 Signed => False );

    if Pipe(Client_Pipe).Opened then
      Console.Write( "Named Client Pipe already opened" );
    else
      -- Form pipe name path with trailing 0 for C
      Pipe(Client_Pipe).Name.Count := 41; -- Ada length
      Pipe(Client_Pipe).Name.Path(1..35) :=
        "/home/clayton/Source/Test/NamedPipe";
      Pipe(Client_Pipe).Name.Path(36..37) := Suffix1.Value(1..2);
      Pipe(Client_Pipe).Name.Path(38..39) := "to";
      Pipe(Client_Pipe).Name.Path(40..41) := Suffix2.Value(1..2);
      Pipe(Client_Pipe).Name.Path(42) := ASCII.NUL; -- for C
      -- Create the named client pipe
      Result := Exec_Itf.mkfifo( Path => Pipe(Client_Pipe).Name.Path'address,
                                 Mode => 8#666# );
      if Result < 0 then -- error
        if Exec_Itf.Get_Error = 17 then
          Result := 0; -- set successful for pipe already exists
        end if;
      end if;

      -- Open the client pipe for transmit
      if Result = 0 then -- no error code returned
        Pipe(Client_Pipe).Handle :=
          Exec_Itf.Open
          ( Path => Pipe(Client_Pipe).Name.Path'address,
            Mode => Exec_Itf.O_RDWR );
        Exec_Itf.Log_Error;
        if to_Int(Pipe(Client_Pipe).Handle) > 0 then -- depending on a small number
          Pipe(Client_Pipe).Opened := True;
        end if;
      end if;

    end if; -- Pipe(Client_Pipe).Opened

    if Pipe(Server_Pipe).Opened then
      Console.Write( "Named Server Pipe already opened" );
    else
      -- Form pipe name path with trailing 0 for C, that is, retain the
      -- Client Pipe name except reverse the application digits.
      Pipe(Server_Pipe).Name := Pipe(Client_Pipe).Name;
      Pipe(Server_Pipe).Name.Path(Pipe(Server_Pipe).Name.Count-5..
                                  Pipe(Server_Pipe).Name.Count-4)
        := Suffix2.Value(1..2);
      Pipe(Server_Pipe).Name.Path(Pipe(Server_Pipe).Name.Count-1..
                                  Pipe(Server_Pipe).Name.Count)
        := Suffix1.Value(1..2);
      Open_Mode := Exec_Itf.O_RDWR;
      if not Wait then
        Open_Mode := Open_Mode + Exec_Itf.O_NDELAY;
      end if;

      -- Open the named pipe to be read
      Pipe(Server_Pipe).Opened := False;
      while not Pipe(Server_Pipe).Opened loop
        Pipe(Server_Pipe).Handle :=
          Exec_Itf.Open
          ( Path => Pipe(Server_Pipe).Name.Path'address,
            Mode => Open_Mode );
        if to_Int(Pipe(Server_Pipe).Handle) > 0 then
          Pipe(Server_Pipe).Opened := True;
          exit; -- loop
        else
          delay 0.25; -- seconds for another app run
        end if;
      end loop;
    end if; -- Pipe(Server_Pipe).Opened

  end Open;

  ------------------------------------------------------------------------------

begin -- initialize at instantiation

  Pipe := ( others => ( Name   => ( Count => 0,
                                    Path  => ( others => ASCII.NUL ) ),
                        Opened => False,
                        Handle => Exec_Itf.Invalid_Pipe_Handle ) );

end Full_Duplex_Pipe;


test1.adb

The portion of the code involved with opening, writing and reading the full-duplex
named pipes and then closing them is illustrated below.

with Console;
with Exec_mC;
with Exec.Text_Log
with Full_Duplex_Pipe;

procedure Test1 is

  Loop_Count
  : Integer := 0;

  package Named_Pipe
  --| Instantiate the named pipe
  is new Full_Duplex_Pipe
         ( Client_App => 1,   -- this test1 application
           Server_App => 2 ); -- the other test2 application

begin

  Console.Initialize; -- output to the terminal and the log file
  Exec.Text_Log.Open;
  Console.Write( "Test1 started" );
  Exec_mC.Initialize;

   . . .

  -- Open the named pipes for full-duplex communication.
  Named_Pipe.Open; -- open client and server pipes to test2
 
   . . .

  while Loop_Count < 3 loop -- start each component 3 times
     . . .
    -- Write "Hi" to the test2 and wait for response.
    declare
      Msg : Named_Pipe.Message_Type;
    begin
      Console.Write("test1 to write Hi", Loop_Count );
      Msg.Count := 2;
      Msg.Data(1..2) := "Hi";
      Named_Pipe.Client.Transmit( Message => Msg ); -- send to test2
      Named_Pipe.Server.Receive( Message => Msg ); -- get response
      Console.Write("test1 received", Msg.Count, Msg.Data(1..Msg.Count) );
    end;

    Loop_Count := Loop_Count + 1;
  end loop;

  . . .

  -- Close named pipes.
  Named_Pipe.Close;

  . . .

end Test1;

test2.adb

with Console;
with Exec_mC;
with Exec.Text_Log;
with Full_Duplex_Pipe;

procedure Test2 is

  Loop_Count
  : Integer := 0;

  package Named_Pipe
  --| Instantiate the named pipe
  is new Full_Duplex_Pipe
         ( Client_App => 2,   -- this test2 application
           Server_App => 1 ); -- the other test1 application

begin

  Console.Initialize; -- output to the terminal and the log file
  Exec.Text_Log.Open;
  Console.Write( "Test1 started" );
  Exec_mC.Initialize;

  . . .

  -- Open named pipes.
  Named_Pipe.Open;

  -- Receive 3 "requests" and respond to each.
  while Loop_Count < 3 loop

   declare
     Msg : Named_Pipe.Message_Type;
    begin
      Named_Pipe.Server.Receive( Message => Msg );
      Console.Write( "Message read from named pipe",
                     Msg.Count,
                     Msg.Data(1..Msg.Count) );
      Msg.Count := 8;
      Msg.Data(1..8) := "Response";
      Named_Pipe.Client.Transmit( Message => Msg );
    end;

    Loop_Count := Loop_Count + 1;
  end loop;
  Console.Write( "test2 after mkpipe reads" );

  -- Close named pipes
  Named_Pipe.Close;

    . . .

  Exec.Text_Log.Close;
  console.write("test2 to exit");

end Test2;

No comments: