I’m looking for a more sophisticated alternative to GNAT.OS_Lib.Spawn to add features to bbt.
I want to feed the process with input, check outputs, ideally separately the standard and the error output, and set environment variables.
I’m not interested at that point in running in // a bunch of processes.
Portability on usual native platforms (Linux, Windows and MacOS) is fine.
I found GNAT.Expect very tempting, but error output doesn’t seems to be separated from normal output, and there is no env variable manipulation, as far as I see.
Alire search spawn (and posix and florist) gave me several results applicable to my case, but Maxim’s Spawn seems to be the only perfect fit.
Before switching to Spawn, I was curious : have you any experience to share with those more or less thick binding to the OS, any advice?
TBH, I try to keep things in standard Ada; this keeps things very portable — for something that is very system dependent, the standard way to do it is to have the system-dependencies collected and abstracted so that the rest of the program “has no idea” about what system it’s running on. (This is usually accomplished by using separate in implementations, selecting the appropriate implementation for the OS via the build-system.)
Ok, so what you’re going to want to do is have three (at least) files — in Ada.Text_IO there’s three standard files/streams: Input, Output, and Error.
procedure Set_Input (File : in File_Type); procedure Set_Output(File : in File_Type); procedure Set_Error (File : in File_Type);
function Standard_Input return File_Type; function Standard_Output return File_Type; function Standard_Error return File_Type;
function Current_Input return File_Type; function Current_Output return File_Type; function Current_Error return File_Type;
Depending on what functionality you want, is whether or not to use these; if, for example, there’s some reasonable expectatoin for foreign-language then using the interface-abstraction of EVIL’s file-type (an auto-closing, trivially interchangeable [[Wide_]Wide_]Character/String text-file, with easy stream-access) would be appropriate.
As for “spawn”, I would counsel asking several questions first, to aid in the design:
Are these disparate, singular items that I can do in Ada, that need to work together somehow?
Use Task.
Are these a series of doing the same thing tasks?
Use Task type.
Are these processes that have the same sort of “shape”?
Use Task interface.
Are these processes that are interfacing with the OS-functionality, have the same “shape”/interface, and are integral to the system/subsystem?
Use task interface + separate and/or generics.
Are the above too much like “hardcoding” and ultimate flexibility is needed?
Then use “spawn”.
I would highly recommend against jumping straight to spawning OS processes for most programs, and if you do have to drop down to that level, I would recommend having some universal/abstracted interface such that “the rest of the program” simply cannot distinguish what OS it’s being run on — is is upfront work, yes; but it does make things much easier to maintain.
Usually spawn is used for running an application from Ada. As an example consider spawning gprbuild to compile and link something and show the output and error messages etc.
Sure, but some people reach for “let me use the command-line”-style spawning to build up a Frankensteinian mismash of a program. (I’ve seen it a couple times with large PHP programs.)
Unix/linux also seem prone to this, and the ref to GNAT.Lib_Spawn and posix/florist raised that warning-flag in my mind.
As a seasoned conspiracy theorist let me ask, do you know the true reason why the Mac mouse had only one button? Because UNIX started a separate process for each mouse button!
In this particular case, I would recommend abstracting everything that is relevant:
A “path”, into a private, possibly limited type.
An “environment” into a String —> String map.
A process-execution into a synchronized interface, possibly with discriminants for the (a) application, (b) parameters and possibly (c) optional [nullable access] environment.
IOW, consider the whole of the “thing to execute” as a high-level abstraction, and use the type-system to implement that, then build upon that as the “what’s expected”.
As I’ve said elsewhere: the best way to use Ada is to use the type-system to descrube the problem, and then write the solution in that.
Depending on the applications you are spawning, you might also have to care about creating pseudo-terminals (tty), or process groups. These make things even more complex.
So in addition to GNAT.Expect, you should also consider using GNAT.Expect.TTY.
Also consider the Err_To_Out parameter in GNAT.Expect.Non_Blocking_Spawn, if you need to separate errors from standard output.
Thanks Emmanuel, I did not correctly interpret the meaning of the Err_To_Out parameter.
I want to keep the full output/input/error stream after run, as files.
I see that there is Get_Input_Fd function returning File_Descriptor, but I don’t see how to manipulate files that way : most operation in OS_Lib expect a file name as input, and not a Fd.
Do you remember how are intended to be used those Get_xxx_Fd functions in Expect ?
I think I would use GNAT.OS_Lib.Read (though I think it might be blocking, so maybe I would also call fcntl() to make it non-blocking, at least on linux). Your external process might itself end up blocking if you do not read fast enough from your end of the pipe. Have a look at the Get_Command_Output function.
The interface is certainly not the most efficient, because it was difficult to make it compatible for both linux and windows, if I remember right. But it seems like this is a core part of your tool, so likely worth spending some time on it,.
The variant Text_Bufferred_Process from GTKAda contributions captures output and error pipes into text buffers. The input pipe is fed from a text buffer too.
If you want for some reason to store into files, then use Asynchronous_Process from GTKAda contributions offers a variant with primitive operations Input, Output, Error. Override Input to read a file, Output and Error to write files.