Linux Exploratory Project Apps Initiated from
Fourth App
Overview:
The Display app of the
Windows version of my Exploratory Project launches the three user / worker apps
of the project. With Windows the use of
the System.Diagnostics.Process.Start function of the Visual C# class starts the
executable that is passed to it and returns its process identifier. Since user apps are DOS, that is
non-windows, applications a new terminal / DOS window is opened for each process
to run in. Its console output is displayed
in its own window and the operator can interact with it via Ada Text_IO input
or terminate it via ctrl-C or terminate them via a Display app option (that is,
to close the user apps via the display app when the display app is being
terminated).
So far I have not found a
way to do the latter (that is, separate terminal windows) while running the
Linux version.
While implementing the
fourth Linux application to start the three user apps of the Exploratory
Project (in preparation for creating a display app using Linux) I found what
first appeared to be a better Linux command than that used in the "Linux
Launch of Other Applications from an Application" post of last month.
Its use will be illustrated after describing a Segmentation
Fault that occurred.
Segmentation Fault:
Whereas the three user apps of the Exploratory Project all
ran successfully in individual terminal windows (see the "Exploratory Project in Linux" post of
a week ago) they failed to run successfully when launched from another application
as had been done for the test apps as described in the post of last month. Instead Segmentation Faults occurred for
each of the EP user apps at seemingly random times.
This was a puzzlement since they had run when launched from
their own terminal windows. At first I
thought it must be because of all the stack space and other memory being used
as the various threads of each application were created (many more threads and
other memory than in the three test applications launched from the fourth application)
and looked for remedies to provide more memory. When I wasn't successful I looked for other ways to have the
fourth application process start the three EP user applications.
I then found some ways that I had missed last month and
tried them with the same result.
However, in doing so I found an internet site of YoLindux that had
posted a "Tutorial for Fork, Exec and Process control" that gave
information on the system command and on the popen command and I decided that
process popen command was much more in line with the single Visual C# Process
Start function of Windows.
But since its use hadn't alleviated the Segmentation Fault I
dug deeper to find the reason. It
turned out that in each application's write to its log file that, since I was using
Ada, I hadn't null terminated the text strings. I don't know why the applications worked when they were run in
their own terminal window and not when they were run from the new fourth
application that was started from a single terminal window. But null terminating the strings fixed the
problem.
[Although, since I was trying other possible fixes as well,
there is the possibility that something else that I did resulted in eliminating
the segmentation faults. But I imagine
that it was in providing the extra nul character since that made the text
strings one character longer and thus prevented the C write file function from
attempting to access a byte in another memory segment.]
Linux popen command:
The YoLinux web site says about popen "The popen()
call opens a process by creating a pipe, forking, and invoking the shell
(bourne shell on Linux). The advantage to using popen() is that it will
allow one to interrogate the results of the command issued." This is why I chose to use it since it did
the fork of the application process and invoked the Linux shell to run the
named command. As with the way I used
last month with the test applications, all the terminal output was displayed to
the one terminal window (or piped to a file included in the command that
launched the fourth application) used to start the fourth application.
This web site "tutorial" also mentioned various
exec functions that can be used to initiate programs from other programs such
as execl() and execlp(), execv() and execvp(), as well as execve().
But the popen also resulted in the Segmentation Faults. I finally determined that this happened when
I passed the executable path directly to popen but not when I passed the script
name for each user app. In the latter
case they ran without the fault. These
scripts are of the form:
#!/bin/bash
/home/clayton/Source/EP/Object1/app1.exe
where the second line of the script is the same path name
that had been passed to popen.
Therefore the use of the shebang ("#!") line would seem to
have been necessary to avoid the segmentation faults.
Since I use a configuration file to provide variable info to
the Exploratory Project apps and this file provides the executable path names I
didn't wish to change to providing a script name and require that there be such
scripts.
GNAT Non_Blocking_Spawn:
Therefore, I returned to the use of the Non_Blocking_Spawn
function contained in the GNAT OS_Lib package that I used in the test
apps.
So that the new app4 application that is being used as a
substitute for the graphical display app won't need to directly reference an
operating system dependent function, I added a new Execute_Program function to
the Exec_Itf package. Therefore, for
either Linux or Windows the display app can invoke the Execute_Program function
and have it launch the user app by its executable path and name.
The Execute_Program function code is:
function
Execute_Program
( Name : in
String
--|
Program name to be executed
) return
PId_t is
-- ++
--| Notes:
--| GNAT s-os_lib.ads for the Spawn procedure
has the following:
-- This procedure spawns a program with a given
list of arguments. The
-- first parameter of is the name of the
executable. The second parameter
-- contains the arguments to be passed to this
program. Success is False
-- if the named program could not be spawned or
its execution completed
-- unsuccessfully. Note that the caller will be
blocked until the
-- execution of the spawned program is
complete. For maximum portability,
-- use a full path name for the Program_Name
argument. On some systems
-- (notably Unix systems) a simple file name
may also work (if the
-- executable can be located in the path).
--
-- Spawning processes from tasking programs is
not recommended. See
--
"NOTE: Spawn in tasking programs" below.
--
-- Note: Arguments in Args that contain spaces
and/or quotes such as
-- "--GCC=gcc -v" or
"--GCC=""gcc -v""" are not portable across all
-- operating systems, and would not have the
desired effect if they were
-- passed directly to the operating system. To
avoid this problem, Spawn
-- makes an internal call to
Normalize_Arguments, which ensures that such
-- arguments are modified in a manner that
ensures that the desired effect
-- is obtained on all operating systems. The
caller may call
-- Normalize_Arguments explicitly before the
call (e.g. to print out the
-- exact form of arguments passed to the
operating system). In this case
-- the guarantee a second call to
Normalize_Arguments has no effect
-- ensures that the internal call will not
affect the result. Note that
-- the implicit call to Normalize_Arguments may
free and reallocate some
-- of the individual arguments.
--
-- This function will always set Success to
False under VxWorks and other
-- similar operating systems which have no
notion of the concept of
-- dynamically executable file. Otherwise
Success is set True if the exit
-- status of the spawned process is zero.
--|
--| Argument_List is a subtype of
String_List. String_List is in
--| s-string.ads as
--| type String_List is array (Positive range
<>) of String_Access;
--| where String_Access is
--| type String_Access is access all String;
-- --
Arg : GNAT.OS_Lib.Argument_List(1..1) := (
others => null );
Arg1 :
GNAT.OS_Lib.String_Access := new String(1..10);
Id : GNAT.OS_Lib.Process_Id;
function
to_PId is new Unchecked_Conversion
( Source => GNAT.OS_Lib.Process_Id,
Target => PId_t
);
begin --
Execute_Program
Arg1.all
:= ( others => ' ' ); -- empty argument
Arg(1) :=
Arg1;
Id :=
GNAT.OS_Lib.Non_Blocking_Spawn( Program_Name => Name,
Args => Arg );
return
to_PId(Id);
end
Execute_Program;
app4 code:
The code for app4 that calls Execute_Program to launch the
three Exploratory Project user applications is as follows.
procedure App4 is
type
App_Configuration_Type
is record
Count : Integer;
--|
Number of user apps in configuration
Handle :
Exec_Itf.File_Handle;
--|
Handle of opened configuration file
Name : String(1..58);
--| Path
and name of configuration file
end record;
App_Configuration
:
App_Configuration_Type
:= (
Count => 0,
--|
Number of user apps in configuration
Handle
=> Exec_Itf.Invalid_File_Handle,
--|
File handle
Name =>
"/home/clayton/Source/EP/Ada/A661UA/Apps-Configuration.dat "
--|
Configuration file path and name
);
subtype
Remote_App_Range_Type
--| Range
of remote user application processes to be launched
is
Apps.User_Application_Id_Type range 1..5;
type
Launched_App_Type
--| Data
about a user application
is record
Running :
Boolean;
--|
Whether user application is running
Id : Exec_Itf.PId_t;
--|
Process id of launched user application
Name : Apps.Configuration.V_String_Type;
--| User
application name
end record;
type Launched_App_Array_Type
is array
(Remote_App_Range_Type) of Launched_App_Type;
Launched_App
:
Launched_App_Array_Type;
Status
:
mC.Exec.Status_Type;
function
Addr_to_Int is new Unchecked_Conversion
( Source => System.Address,
Target => Integer );
begin
Console.Initialize( App_Id => '4' ); -- output to the terminal and
the log file
Exec.Text_Log.Initialize;
Exec.Text_Log.Open( App => ( Name => "App4 ",
Id => 4 ) );
Console.Write( "App4 started" );
mC.Exec.Initialize( App_Id => 4 );
declare
Current_Time : Exec_Itf.OS_Time;
File_Time : Exec_Itf.OS_Time;
Split_Time :
Exec_Itf.Split_Time_Type;
App_Name
: String(1..4) := "app4";
Name : String(1..60);
function
Time_to_Int is new Unchecked_Conversion
( Source => Exec_Itf.OS_Time,
Target => Integer );
begin
Current_Time := Exec_Itf.Current_Time;
Console.Write( "Current_Time", Time_to_Int(Current_Time) );
Name(1..24) := "/home/clayton/Source/EP/";
Name(25..5+23) := String(App_Name(1..4));
Name(4+25..4+28) := ".log";
for I in
4+29..Name'length loop
Name(I)
:= ASCII.NUL;
end loop;
File_Time
:= Exec_Itf.File_Time_Stamp( Name => Name );
Console.Write( "File_Time", Time_to_Int(File_Time) );
Split_Time := Exec_Itf.System_Time( Time => File_Time );
Console.Write( "System Date", Integer(Split_Time.Year),
Integer(Split_Time.Month),
Integer(Split_Time.Day) );
Console.Write( "System Time", Integer(Split_Time.Hour),
Integer(Split_Time.Minute),
Integer(Split_Time.Second) );
end;
-- Get
thread id of this main thread and display.
declare
Main_Thread_Id : mC.Exec.Thread_Id_Ext_Type;
Process_Id : Exec_Itf.PId_t;
function
to_Int is new Unchecked_Conversion
( Source => Exec_Itf.PId_t,
Target => Integer );
begin
mC.Exec.My_Thread_Id
(
Thread_Id => Main_Thread_Id,
Status => Status );
Process_Id := Exec_Itf.GetPId;
Console.Write("Main process
id",to_Int(Process_Id));
end;
--|
Logic_Step:
--| Obtain user application paths and names
from Configuration file.
for I in
Remote_App_Range_Type loop
Launched_App(I).Running := False;
Launched_App(I).Id := Exec_Itf.Invalid_PId;
Launched_App(I).Name.Count := 0;
Launched_App(I).Name.Data := ( others => ASCII.NUL );
end loop;
-- null
terminate the path name for use by C.
App_Configuration.Name(App_Configuration.Name'length) := ASCII.NUL;
-- open the
configuration file.
App_Configuration.Handle := Exec_Itf.Open_Read
( Name => App_Configuration.Name
);
declare
App : Remote_App_Range_Type := 1;
Bytes : Integer;
Fields :
Integer;
Idx : Integer;
Index : Integer;
Line : String(1..1000);
use type
Apps.Component_Selector_Type;
function
to_Int is new Unchecked_Conversion
( Source => Exec_Itf.PId_t,
Target => Integer );
begin
--|
Logic_Step:
--| Obtain number of user apps from
configuration file.
Bytes :=
Exec_Itf.Read_File -- read first
line with number of user apps
( File => App_Configuration.Handle,
Addr => Line'address,
Num => 13 );
if Bytes
> 2 then
-- Find
first field separator
for I
in 1..Bytes loop
if
Line(I) = '|' then
--
Convert first field to numeric
App_Configuration.Count := Numeric_Conversion.ASCII_to_Integer
( Num_String =>
Line(1..I-1) );
exit; -- loop
end
if;
end
loop;
end if;
--|
Logic_Step:
--| Obtain user app path and executable name.
Bytes :=
Exec_Itf.Read_File
( File =>
App_Configuration.Handle,
Addr => Line'address,
Num => 1000 );
Idx := 1;
-- index into Line
if Bytes
> 1 then
Fields
:= 0;
Index
:= 0;
Parse:
while
Idx <= Bytes loop
-- skip past first 10 field separators of
each "record"
if
Line(Idx) = '|' then
Fields := Fields + 1;
if
Fields = 11 then -- separator following path name
Launched_App(App).Name.Data(Index+1) := ASCII.NUL; -- trailing NUL
Launched_App(App).Name.Count := Index;
Fields := 0;
Index := 0;
if App < Remote_App_Range_Type'last then
App := App + 1;
else
exit Parse; -- loop
end if;
end
if; -- Fields = 11
elsif
Fields = 10 then -- not separator, capture path
Index := Index + 1;
Launched_App(App).Name.Data(Index) := Line(Idx);
end
if; -- separator character
Idx
:= Idx + 1;
end
loop Parse;
end if;
-- Bytes > 1
end;
-- Spawn /
launch the other three application processes via this one.
declare
begin
for I in
Remote_App_Range_Type loop
if
Launched_App(I).Name.Count > 0 then
Launched_App(I).Id := Exec_Itf.Execute_Program
( Name =>
Launched_App(I).Name.Data );
delay
0.2; -- allow the app to start for doing next
end if;
end loop;
end;
--|
Logic_Step:
--| Count number of launched applications.
declare
Count
:
Integer;
Launched_Count
:
Integer;
begin
Count :=
0;
Launched_Count := 0;
for I in
Remote_App_Range_Type loop
if
Launched_App(I).Name.Count > 0 then
Launched_App(I).Running := True; -- assume running
Launched_Count := Launched_Count + 1;
end if;
end loop;
delay
0.5; -- Allow launched applications to start
--|
Logic_Step:
--| Wait for all the launched applications to
finish.
Forever:
loop
-- Wait for each launched app to finish
running.
for I
in Remote_App_Range_Type loop
if
Launched_App(I).Running then
if
not Exec_Itf.Is_Running( Path => Launched_App(I).Name.Data ) then
Launched_App(I).Running := False;
Count := Count + 1;
exit Forever when Count = Launched_Count;
end
if;
end
if;
end
loop;
delay
0.2; -- to allow other applications to run
end loop
Forever;
end;
--|
Logic_Step:
--| Terminate.
-- Kill the
threads and the lock semaphore.
--
Exec_mC.Destroy;
Exec.Text_Log.Close;
Exec_Itf.Exit_to_OS;
end App4;
Another problem:
Since app4 does not, as yet, communicate with the user apps
as it must for the A661 display app I have the user apps exiting when a counter
reaches a particular value. When app4
is changed to the display app, closing the display app will cause it to close
the user apps. In the meantime, the
Linux named pipes remain in existence and seemingly continue to contain any
data written to them but not yet read by the receiving app as it is closed -
such as when the app is closed via a ctrl-C.
Therefore, I changed the script to start app4 to delete the named pipes
before running app4. As follows:
#!/bin/bash
# delete the named pipes created by the three user
apps
rm /home/clayton/Source/EP/app1to2
rm /home/clayton/Source/EP/app1to3
rm /home/clayton/Source/EP/app2to1
rm /home/clayton/Source/EP/app2to3
rm /home/clayton/Source/EP/app3to1
rm /home/clayton/Source/EP/app3to2
# run the app that runs the three user apps
/home/clayton/Source/EP/Object4/app4.exe