Skip to content

Commit 7ab1a5b

Browse files
authoredFeb 25, 2025··
system: subprocessing interface (#911)
2 parents 76292d3 + 0675b8c commit 7ab1a5b

22 files changed

+2291
-39
lines changed
 

‎CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.14.0)
44
set(CMAKE_USER_MAKE_RULES_OVERRIDE ${CMAKE_CURRENT_SOURCE_DIR}/config/DefaultFlags.cmake)
55

66
project(fortran_stdlib
7-
LANGUAGES Fortran
7+
LANGUAGES Fortran C
88
DESCRIPTION "Community driven and agreed upon de facto standard library for Fortran"
99
)
1010

‎config/fypp_deployment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def recursive_copy(folder):
105105
for root, _, files in os.walk(folder):
106106
for file in files:
107107
if file not in prune:
108-
if file.endswith(".f90") or file.endswith(".F90") or file.endswith(".dat") or file.endswith(".npy"):
108+
if file.endswith((".f90", ".F90", ".dat", ".npy", ".c")):
109109
shutil.copy2(os.path.join(root, file), base_folder+os.sep+folder+os.sep+file)
110110
recursive_copy('src')
111111
recursive_copy('test')

‎doc/specs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ This is an index/directory of the specifications (specs) for each new module/fea
3838
- [string\_type](./stdlib_string_type.html) - Basic string support
3939
- [stringlist_type](./stdlib_stringlist_type.html) - 1-Dimensional list of strings
4040
- [strings](./stdlib_strings.html) - String handling and manipulation routines
41+
- [system](./stdlib_system.html) - OS and sub-processing routines
4142
- [version](./stdlib_version.html) - Version information
4243

4344
## Released/Stable Features & Modules

‎doc/specs/stdlib_strings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ The result is of the same type as the elements of `strings` (`type(string_type)`
498498
```
499499

500500
<!-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->
501+
501502
### `to_string`
502503

503504
#### Description

‎doc/specs/stdlib_system.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
---
2+
title: system
3+
---
4+
5+
# System and sub-processing module
6+
7+
The `stdlib_system` module provides interface for interacting with external processes, enabling the execution
8+
and monitoring of system commands or applications directly from Fortran.
9+
10+
[TOC]
11+
12+
## `run` - Execute an external process synchronously
13+
14+
### Status
15+
16+
Experimental
17+
18+
### Description
19+
20+
The `run` interface allows execution of external processes using a single command string or a list of arguments.
21+
Processes run synchronously, meaning execution is blocked until the process finishes.
22+
Optional arguments enable the collection of standard output and error streams, as well as sending input via standard input.
23+
Additionally, a callback function can be specified to execute upon process completion, optionally receiving a user-defined payload.
24+
25+
### Syntax
26+
27+
`process = ` [[stdlib_subprocess(module):run(interface)]] `(args [, stdin] [, want_stdout] [, want_stderr] [, callback] [, payload])`
28+
29+
### Arguments
30+
31+
`args`: Shall be a `character(*)` string (for command-line execution) or a `character(*), dimension(:)` array (for argument-based execution). It specifies the command and arguments to execute. This is an `intent(in)` argument.
32+
33+
`stdin` (optional): Shall be a `character(*)` value containing input to send to the process via standard input (pipe). This is an `intent(in)` argument.
34+
35+
`want_stdout` (optional): Shall be a `logical` flag. If `.true.`, the standard output of the process will be captured; if `.false.` (default), it will be lost. This is an `intent(in)` argument.
36+
37+
`want_stderr` (optional): Shall be a `logical` flag. If `.true.`, the standard error output of the process will be captured. If `.false.` (default), it will be lost. This is an `intent(in)` argument.
38+
39+
`callback` (optional): Shall be a procedure conforming to the `process_callback` interface. If present, this function will be called upon process completion with the process ID, exit state, and optionally collected standard input, output, and error streams. This is an `intent(in)` argument.
40+
41+
`payload` (optional): Shall be a generic (`class(*)`) scalar that will be passed to the callback function upon process completion. It allows users to associate custom data with the process execution. This is an `intent(inout), target` argument.
42+
43+
### Return Value
44+
45+
Returns an object of type `process_type` that contains information about the state of the created process.
46+
47+
### Example
48+
49+
```fortran
50+
! Example usage with command line or list of arguments
51+
type(process_type) :: p
52+
53+
! Run a simple command line synchronously
54+
p = run("echo 'Hello, world!'", want_stdout=.true.)
55+
```
56+
57+
## `runasync` - Execute an external process asynchronously
58+
59+
### Status
60+
61+
Experimental
62+
63+
### Description
64+
65+
The `runasync` interface allows execution of external processes using a single command string or a list of arguments.
66+
Processes are run asynchronously (non-blocking), meaning execution does not wait for the process to finish.
67+
Optional arguments enable the collection of standard output and error streams, as well as sending input via standard input.
68+
Additionally, a callback function can be specified to execute upon process completion, optionally receiving a user-defined payload.
69+
70+
### Syntax
71+
72+
`process = ` [[stdlib_subprocess(module):runasync(interface)]] `(args [, stdin] [, want_stdout] [, want_stderr] [, callback] [, payload])`
73+
74+
### Arguments
75+
76+
`args`: Shall be a `character(*)` string (for command-line execution) or a `character(*), dimension(:)` array (for argument-based execution). It specifies the command and arguments to execute. This is an `intent(in)` argument.
77+
78+
`stdin` (optional): Shall be a `character(*)` value containing input to send to the process via standard input (pipe). This is an `intent(in)` argument.
79+
80+
`want_stdout` (optional): Shall be a `logical` flag. If `.true.`, the standard output of the process will be captured; if `.false.` (default), it will be lost. This is an `intent(in)` argument.
81+
82+
`want_stderr` (optional): Shall be a `logical` flag. If `.true.`, the standard error output of the process will be captured. Default: `.false.`. This is an `intent(in)` argument.
83+
84+
`callback` (optional): Shall be a procedure conforming to the `process_callback` interface. If present, this function will be called upon process completion with the process ID, exit state, and optionally collected standard input, output, and error streams. This is an `intent(in)` argument.
85+
86+
`payload` (optional): Shall be a generic (`class(*)`) scalar that will be passed to the callback function upon process completion. It allows users to associate custom data with the process execution. This is an `intent(inout), target` argument.
87+
88+
### Return Value
89+
90+
Returns an object of type `process_type` that contains information about the state of the created process.
91+
92+
### Example
93+
94+
```fortran
95+
{!example/system/example_process_1.f90!}
96+
```
97+
98+
## `is_running` - Check if a process is still running
99+
100+
### Status
101+
102+
Experimental
103+
104+
### Description
105+
106+
The `is_running` interface provides a method to check if an external process is still running.
107+
This is useful for monitoring the status of asynchronous processes created with the `run` interface.
108+
109+
### Syntax
110+
111+
`status = ` [[stdlib_subprocess(module):is_running(interface)]] `(process)`
112+
113+
### Arguments
114+
115+
`process`: Shall be a `type(process_type)` object representing the external process to check. This is an `intent(inout)` argument.
116+
117+
118+
### Return Value
119+
120+
Returns a `logical` value: `.true.` if the process is still running, or `.false.` if the process has terminated.
121+
After a call to `is_running`, the `type(process_type)` structure is also updated to the latest process state.
122+
123+
### Example
124+
125+
```fortran
126+
{!example/system/example_process_2.f90!}
127+
```
128+
129+
## `is_completed` - Check if a process has completed execution
130+
131+
### Status
132+
133+
Experimental
134+
135+
### Description
136+
137+
The `is_completed` interface provides a method to check if an external process has finished execution.
138+
This is useful for determining whether asynchronous processes created with the `run` interface have terminated.
139+
140+
### Syntax
141+
142+
`status = ` [[stdlib_subprocess(module):is_completed(interface)]] `(process)`
143+
144+
### Arguments
145+
146+
`process`: Shall be a `type(process_type)` object representing the external process to check. This is an `intent(inout)` argument.
147+
148+
### Return Value
149+
150+
Returns a `logical` value:
151+
- `.true.` if the process has completed.
152+
- `.false.` if the process is still running.
153+
154+
After a call to `is_completed`, the `type(process_type)` structure is updated to reflect the latest process state.
155+
156+
### Example
157+
158+
```fortran
159+
{!example/system/example_process_1.f90!}
160+
```
161+
162+
## `elapsed` - Return process lifetime in seconds
163+
164+
### Status
165+
166+
Experimental
167+
168+
### Description
169+
170+
The `elapsed` interface provides a method to calculate the total time that has elapsed since a process was started.
171+
This is useful for tracking the duration of an external process or for performance monitoring purposes.
172+
173+
The result is a real value representing the elapsed time in seconds, measured from the time the process was created.
174+
175+
### Syntax
176+
177+
`delta_t = ` [[stdlib_subprocess(module):elapsed(subroutine)]] `(process)`
178+
179+
### Arguments
180+
181+
`process`: Shall be a `type(process_type)` object representing the external process. It is an `intent(in)` argument.
182+
183+
### Return Value
184+
185+
Returns a `real(real64)` value that represents the elapsed time (in seconds) since the process was started.
186+
If the process is still running, the value returned is the time elapsed until the call to this function.
187+
Otherwise, the total process duration from creation until completion is returned.
188+
189+
### Example
190+
191+
```fortran
192+
{!example/system/example_process_3.f90!}
193+
```
194+
195+
## `wait` - Wait until a running process is completed
196+
197+
### Status
198+
199+
Experimental
200+
201+
### Description
202+
203+
The `wait` interface provides a method to block the calling program until the specified process completes.
204+
If the process is running asynchronously, this subroutine will pause the workflow until the given process finishes.
205+
Additionally, an optional maximum wait time can be provided. If the process does not finish within the specified time,
206+
the subroutine will return without waiting further.
207+
208+
On return from this routine, the process state is accordingly updated.
209+
This is useful when you want to wait for a background task to complete, but want to avoid indefinite blocking
210+
in case of process hang or delay.
211+
212+
213+
### Syntax
214+
215+
`call ` [[stdlib_subprocess(module):wait(subroutine)]] `(process [, max_wait_time])`
216+
217+
### Arguments
218+
219+
`process`: Shall be a `type(process_type)` object representing the external process to monitor.
220+
This is an `intent(inout)` argument, and its state is updated upon completion.
221+
222+
`max_wait_time` (optional): Shall be a `real` value specifying the maximum wait time in seconds.
223+
If not provided, the subroutine will wait indefinitely until the process completes.
224+
225+
### Example
226+
227+
```fortran
228+
{!example/system/example_process_2.f90!}
229+
```
230+
231+
## `update` - Update the internal state of a process
232+
233+
### Status
234+
235+
Experimental
236+
237+
### Description
238+
239+
The `update` interface allows the internal state of a process object to be updated by querying the system.
240+
After the process completes, the standard output and standard error are retrieved, if they were requested, and loaded into the `process%stdout` and `process%stderr` string variables, respectively.
241+
242+
This is especially useful for monitoring asynchronous processes and retrieving their output after they have finished.
243+
244+
### Syntax
245+
246+
`call ` [[stdlib_subprocess(module):update(subroutine)]] `(process)`
247+
248+
### Arguments
249+
250+
`process`: Shall be a `type(process_type)` object representing the external process whose state needs to be updated.
251+
This is an `intent(inout)` argument, and its internal state is updated on completion.
252+
253+
### Example
254+
255+
```fortran
256+
{!example/system/example_process_5.f90!}
257+
```
258+
259+
## `kill` - Terminate a running process
260+
261+
### Status
262+
263+
Experimental
264+
265+
### Description
266+
267+
The `kill` interface is used to terminate a running external process. It attempts to stop the process and returns a boolean flag indicating whether the operation was successful.
268+
This interface is useful when a process needs to be forcefully stopped, for example, if it becomes unresponsive or if its execution is no longer required.
269+
270+
### Syntax
271+
272+
`call ` [[stdlib_subprocess(module):kill(subroutine)]] `(process, success)`
273+
274+
### Arguments
275+
276+
`process`: Shall be a `type(process_type)` object representing the external process to be terminated.
277+
This is an `intent(inout)` argument, and on return is updated with the terminated process state.
278+
279+
`success`: Shall be a `logical` variable. It is set to `.true.` if the process was successfully killed, or `.false.` otherwise.
280+
281+
### Example
282+
283+
```fortran
284+
{!example/system/example_process_4.f90!}
285+
```
286+
287+
## `sleep` - Pause execution for a specified time in milliseconds
288+
289+
### Status
290+
291+
Experimental
292+
293+
### Description
294+
295+
The `sleep` interface pauses the execution of a program for a specified duration, given in milliseconds.
296+
This routine acts as a cross-platform wrapper, abstracting the underlying platform-specific sleep implementations.
297+
It ensures that the requested sleep duration is honored on both Windows and Unix-like systems.
298+
299+
### Syntax
300+
301+
`call ` [[stdlib_system(module):sleep(subroutine)]] `(millisec)`
302+
303+
### Arguments
304+
305+
`millisec`: Shall be an `integer` representing the number of milliseconds to sleep. This is an `intent(in)` argument.
306+
307+
### Example
308+
309+
```fortran
310+
{!example/system/example_sleep.f90!}
311+
```
312+
313+
## `is_windows` - Check if the system is running on Windows
314+
315+
### Status
316+
317+
Experimental
318+
319+
### Description
320+
321+
The `is_windows` interface provides a quick, compile-time check to determine if the current system is Windows.
322+
It leverages a C function that checks for the presence of the `_WIN32` macro, which is defined in C compilers when targeting Windows.
323+
This function is highly efficient and works during the compilation phase, avoiding the need for runtime checks.
324+
325+
### Syntax
326+
327+
`result = ` [[stdlib_system(module):is_windows(function)]] `()`
328+
329+
### Return Value
330+
331+
Returns a `logical` flag: `.true.` if the system is Windows, or `.false.` otherwise.
332+
333+
### Example
334+
335+
```fortran
336+
{!example/system/example_process_1.f90!}
337+
```

‎example/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ add_subdirectory(stats_distribution_uniform)
3030
add_subdirectory(stringlist_type)
3131
add_subdirectory(strings)
3232
add_subdirectory(string_type)
33+
add_subdirectory(system)
3334
add_subdirectory(version)

‎example/system/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ADD_EXAMPLE(process_1)
2+
ADD_EXAMPLE(process_2)
3+
ADD_EXAMPLE(process_3)
4+
ADD_EXAMPLE(process_4)
5+
ADD_EXAMPLE(process_5)
6+
ADD_EXAMPLE(process_6)
7+
ADD_EXAMPLE(process_7)
8+
ADD_EXAMPLE(sleep)

‎example/system/example_process_1.f90

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
! Process example 1: Run a Command Synchronously and Capture Output
2+
program run_sync
3+
use stdlib_system, only: run, is_completed, is_windows, process_type
4+
implicit none
5+
6+
type(process_type) :: p
7+
logical :: completed
8+
9+
! Run a synchronous process to list directory contents
10+
if (is_windows()) then
11+
p = run("dir", want_stdout=.true.)
12+
else
13+
p = run("ls -l", want_stdout=.true.)
14+
end if
15+
16+
! Check if the process is completed (should be true since wait=.true.)
17+
if (is_completed(p)) then
18+
print *, "Process completed successfully. The current directory: "
19+
print *, p%stdout
20+
else
21+
print *, "Process is still running (unexpected)."
22+
end if
23+
24+
end program run_sync

‎example/system/example_process_2.f90

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
! Process example 2: Run an Asynchronous Command and check its status
2+
program run_async
3+
use stdlib_system, only: process_type, runasync, is_running, wait
4+
implicit none
5+
6+
type(process_type) :: p
7+
8+
! Run an asynchronous process to sleep for 1 second
9+
p = runasync("sleep 1")
10+
11+
! Check if the process is running
12+
if (is_running(p)) then
13+
print *, "Process is running."
14+
else
15+
print *, "Process has already completed."
16+
end if
17+
18+
! Wait for the process to complete
19+
call wait(p, max_wait_time = 5.0)
20+
print *, "Process has now completed."
21+
end program run_async

‎example/system/example_process_3.f90

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
! Process example 3: Run with many arguments, and check runtime
2+
program run_with_args
3+
use stdlib_system, only: process_type, run, elapsed, wait
4+
implicit none
5+
6+
type(process_type) :: p
7+
character(len=15), allocatable :: args(:)
8+
9+
! Define arguments for the `echo` command
10+
allocate(args(2))
11+
args(1) = "echo"
12+
args(2) = "Hello, Fortran!"
13+
14+
! Run the command with arguments (synchronous)
15+
p = run(args)
16+
17+
! Print the runtime of the process
18+
print *, "Process runtime:", elapsed(p), "seconds."
19+
20+
! Clean up
21+
deallocate(args)
22+
end program run_with_args

‎example/system/example_process_4.f90

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
! Process example 4: Kill a running process
2+
program example_process_kill
3+
use stdlib_system, only: process_type, runasync, is_running, kill, elapsed, is_windows, sleep
4+
implicit none
5+
type(process_type) :: process
6+
logical :: running, success
7+
8+
print *, "Starting a long-running process..."
9+
if (is_windows()) then
10+
process = runasync("ping -n 10 127.0.0.1")
11+
else
12+
process = runasync("ping -c 10 127.0.0.1")
13+
endif
14+
15+
! Verify the process is running
16+
running = is_running(process)
17+
print *, "Process running:", running
18+
19+
! Wait a bit before killing the process
20+
call sleep(millisec=250)
21+
22+
print *, "Killing the process..."
23+
call kill(process, success)
24+
25+
if (success) then
26+
print *, "Process killed successfully."
27+
else
28+
print *, "Failed to kill the process."
29+
endif
30+
31+
! Verify the process is no longer running
32+
running = is_running(process)
33+
print *, "Process running after kill:", running
34+
35+
end program example_process_kill

‎example/system/example_process_5.f90

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
! Process example 5: Object-oriented interface
2+
program example_process_5
3+
use stdlib_system, only: process_type, runasync, is_windows, sleep, update
4+
implicit none
5+
type(process_type) :: process
6+
7+
if (is_windows()) then
8+
process = runasync("ping -n 10 127.0.0.1")
9+
else
10+
process = runasync("ping -c 10 127.0.0.1")
11+
endif
12+
13+
! Verify the process is running
14+
do while (process%is_running())
15+
16+
! Update process state
17+
call update(process)
18+
19+
! Wait a bit before killing the process
20+
call sleep(millisec=1500)
21+
22+
print *, "Process has been running for ",process%elapsed()," seconds..."
23+
24+
end do
25+
26+
print *, "Process ",process%pid()," completed in ",process%elapsed()," seconds."
27+
28+
end program example_process_5

‎example/system/example_process_6.f90

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
! Process example 6: Demonstrate callback
2+
program example_process_6
3+
use stdlib_system, only: process_type, process_ID, run, is_running, kill, elapsed, is_windows, sleep
4+
implicit none
5+
type(process_type) :: p
6+
integer, target :: nfiles
7+
8+
! Run process, attach callback function and some data
9+
if (is_windows()) then
10+
p = run("dir",want_stdout=.true.,callback=get_dir_nfiles)
11+
else
12+
p = run("ls -l",want_stdout=.true.,callback=get_dir_nfiles,payload=nfiles)
13+
endif
14+
15+
! On exit, the number of files should have been extracted by the callback function
16+
print *, "Current directory has ",nfiles," files"
17+
18+
contains
19+
20+
! Custom callback function: retrieve number of files from ls output
21+
subroutine get_dir_nfiles(pid, exit_state, stdin, stdout, stderr, payload)
22+
integer(process_ID), intent(in) :: pid
23+
integer, intent(in) :: exit_state
24+
character(len=*), optional, intent(in) :: stdin, stdout, stderr
25+
class(*), optional, intent(inout) :: payload
26+
27+
integer :: i
28+
29+
if (present(payload)) then
30+
31+
select type (nfiles => payload)
32+
type is (integer)
33+
if (present(stdout)) then
34+
nfiles = count([ (stdout(i:i) == char(10), i=1,len(stdout)) ])
35+
else
36+
nfiles = -1
37+
endif
38+
class default
39+
error stop 'Wrong payload passed to the process'
40+
end select
41+
42+
end if
43+
end subroutine get_dir_nfiles
44+
45+
end program example_process_6

‎example/system/example_process_7.f90

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
! Process example 7: Usage of `kill`
2+
program example_process_7
3+
use stdlib_system, only: process_type, runasync, kill
4+
implicit none
5+
6+
type(process_type) :: p
7+
logical :: success
8+
9+
! Start a process asynchronously
10+
p = runasync("sleep 10")
11+
12+
! Attempt to kill the process
13+
call kill(p, success)
14+
15+
if (success) then
16+
print *, "Process successfully killed."
17+
else
18+
print *, "Failed to kill the process."
19+
end if
20+
21+
end program example_process_7

‎example/system/example_sleep.f90

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
! Usage of `sleep`
2+
program example_sleep
3+
use stdlib_system, only: sleep
4+
implicit none
5+
6+
print *, "Starting sleep..."
7+
8+
! Sleep for 500 milliseconds
9+
call sleep(500)
10+
11+
print *, "Finished sleeping!"
12+
13+
end program example_sleep

‎src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ set(SRC
109109
stdlib_hashmap_open.f90
110110
stdlib_logger.f90
111111
stdlib_sorting_radix_sort.f90
112+
stdlib_system_subprocess.c
113+
stdlib_system_subprocess.F90
112114
stdlib_system.F90
113115
stdlib_sparse.f90
114116
stdlib_specialfunctions.f90

‎src/stdlib_strings.fypp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ module stdlib_strings
185185
module procedure :: join_char
186186
end interface join
187187

188-
189188
contains
190189

191190

‎src/stdlib_system.F90

Lines changed: 420 additions & 36 deletions
Large diffs are not rendered by default.

‎src/stdlib_system_subprocess.F90

Lines changed: 763 additions & 0 deletions
Large diffs are not rendered by default.

‎src/stdlib_system_subprocess.c

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
#include <sys/types.h>
2+
#include <stdio.h>
3+
#include <stdlib.h>
4+
#include <stdbool.h>
5+
#include <stdint.h>
6+
#include <string.h>
7+
8+
#ifdef _WIN32
9+
#include <windows.h>
10+
#else
11+
#define _POSIX_C_SOURCE 199309L
12+
#include <sys/wait.h>
13+
#include <unistd.h>
14+
#include <time.h>
15+
#include <errno.h>
16+
#include <signal.h>
17+
#endif // _WIN32
18+
19+
// Typedefs
20+
typedef void* stdlib_handle;
21+
typedef int64_t stdlib_pid;
22+
23+
24+
/////////////////////////////////////////////////////////////////////////////////////
25+
// Windows-specific code
26+
/////////////////////////////////////////////////////////////////////////////////////
27+
#ifdef _WIN32
28+
29+
// On Windows systems: create a new process
30+
void process_create_windows(const char* cmd, const char* stdin_stream,
31+
const char* stdin_file, const char* stdout_file, const char* stderr_file,
32+
stdlib_pid* pid) {
33+
34+
STARTUPINFO si;
35+
PROCESS_INFORMATION pi;
36+
HANDLE hStdout = NULL, hStderr = NULL;
37+
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
38+
FILE* stdin_fp = NULL;
39+
40+
// Initialize null handle
41+
(*pid) = 0;
42+
43+
ZeroMemory(&si, sizeof(si));
44+
si.cb = sizeof(STARTUPINFO);
45+
46+
// If possible, we redirect stdout/stderr to file handles directly.
47+
// This will override any cmd redirection settings (<>). For stdin
48+
49+
// Write stdin_stream to stdin_file if provided
50+
if (stdin_stream && stdin_file) {
51+
stdin_fp = fopen(stdin_file, "w");
52+
if (!stdin_fp) {
53+
fprintf(stderr, "Failed to open stdin file for writing\n");
54+
return;
55+
}
56+
fputs(stdin_stream, stdin_fp);
57+
fclose(stdin_fp);
58+
}
59+
60+
// Open stdout file if provided, otherwise use the null device
61+
if (stdout_file) {
62+
hStdout = CreateFile(stdout_file, GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
63+
if (hStdout == INVALID_HANDLE_VALUE) {
64+
fprintf(stderr, "Failed to open stdout file\n");
65+
return;
66+
}
67+
} else {
68+
hStdout = CreateFile("NUL", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
69+
if (hStdout == INVALID_HANDLE_VALUE) {
70+
fprintf(stderr, "Failed to open null device for stdout\n");
71+
return;
72+
}
73+
}
74+
si.hStdOutput = hStdout;
75+
si.dwFlags |= STARTF_USESTDHANDLES;
76+
77+
// Open stderr file if provided, otherwise use the null device
78+
if (stderr_file) {
79+
hStderr = CreateFile(stderr_file, GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
80+
if (hStderr == INVALID_HANDLE_VALUE) {
81+
fprintf(stderr, "Failed to open stderr file\n");
82+
return;
83+
}
84+
} else {
85+
hStderr = CreateFile("NUL", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
86+
if (hStderr == INVALID_HANDLE_VALUE) {
87+
fprintf(stderr, "Failed to open null device for stderr\n");
88+
return;
89+
}
90+
}
91+
si.hStdError = hStderr;
92+
si.dwFlags |= STARTF_USESTDHANDLES;
93+
94+
// Prepare the command line with redirected stdin
95+
char* full_cmd;
96+
size_t cmd_len = strlen(cmd);
97+
size_t stdin_len = stdin_file ? strlen(stdin_file) : 0;
98+
size_t full_cmd_len = cmd_len + stdin_len + 5;
99+
full_cmd = (char*)malloc(full_cmd_len);
100+
if (!full_cmd) {
101+
fprintf(stderr, "Failed to allocate memory for full_cmd\n");
102+
return;
103+
}
104+
105+
// Use full_cmd as needed (e.g., pass to CreateProcess)
106+
if (stdin_file) {
107+
snprintf(full_cmd, full_cmd_len, "%s < %s", cmd, stdin_file);
108+
} else {
109+
snprintf(full_cmd, full_cmd_len, "%s", cmd);
110+
}
111+
112+
// Create the process
113+
BOOL success = CreateProcess(
114+
NULL, // Application name
115+
full_cmd, // Command line
116+
NULL, // Process security attributes
117+
NULL, // Thread security attributes
118+
TRUE, // Inherit handles
119+
0, // Creation flags
120+
NULL, // Environment variables
121+
NULL, // Current directory
122+
&si, // STARTUPINFO
123+
&pi // PROCESS_INFORMATION
124+
);
125+
126+
// Free the allocated memory
127+
free(full_cmd);
128+
129+
if (!success) {
130+
fprintf(stderr, "CreateProcess failed (%lu).\n", GetLastError());
131+
return;
132+
}
133+
134+
// Close unneeded handles
135+
if (hStdout) CloseHandle(hStdout);
136+
if (hStderr) CloseHandle(hStderr);
137+
138+
// Return the process handle for status queries
139+
CloseHandle(pi.hThread); // Close the thread handle
140+
(*pid) = (stdlib_pid) pi.dwProcessId;
141+
142+
}
143+
144+
// Query process state on a Windows system
145+
void process_query_status_windows(stdlib_pid pid, bool wait, bool* is_running, int* exit_code)
146+
{
147+
int wait_code;
148+
HANDLE hProcess;
149+
DWORD dwExitCode,dwPid;
150+
151+
dwPid = (DWORD) pid;
152+
153+
// Open the process with the appropriate access rights
154+
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, dwPid);
155+
156+
// Error opening the process, likely pid does not exist
157+
if (hProcess == NULL) {
158+
*is_running = false;
159+
*exit_code = -1;
160+
return;
161+
}
162+
163+
164+
if (wait) {
165+
// Wait for the process to terminate
166+
wait_code = WaitForSingleObject(hProcess, INFINITE);
167+
} else {
168+
// Check if the process has terminated
169+
wait_code = WaitForSingleObject(hProcess, 0);
170+
}
171+
172+
if (wait_code == WAIT_OBJECT_0) {
173+
// Process has exited, get the exit code
174+
*is_running = false;
175+
if (GetExitCodeProcess(hProcess, &dwExitCode)) {
176+
*exit_code = dwExitCode;
177+
} else {
178+
*exit_code = -1; // Error retrieving the exit code
179+
}
180+
} else if (wait_code == WAIT_TIMEOUT) {
181+
// Process is still running
182+
*is_running = true;
183+
*exit_code = 0;
184+
} else { // WAIT_FAILED
185+
// Error occurred
186+
*is_running = false;
187+
*exit_code = -1; // Error occurred in WaitForSingleObject
188+
}
189+
190+
// Close the process handle
191+
CloseHandle(hProcess);
192+
}
193+
194+
// Kill a process on Windows by sending a PROCESS_TERMINATE signal.
195+
// Return true if the operation succeeded, or false if it failed (process does not
196+
// exist anymore, or we may not have the rights to kill the process).
197+
bool process_kill_windows(stdlib_pid pid) {
198+
HANDLE hProcess;
199+
DWORD dwPid;
200+
201+
dwPid = (DWORD) pid;
202+
203+
// Open the process with terminate rights
204+
hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwPid);
205+
206+
if (hProcess == NULL) {
207+
// Failed to open the process; return false
208+
return false;
209+
}
210+
211+
// Attempt to terminate the process
212+
if (!TerminateProcess(hProcess, 1)) {
213+
// Failed to terminate the process
214+
CloseHandle(hProcess);
215+
return false;
216+
}
217+
218+
// Successfully terminated the process
219+
CloseHandle(hProcess);
220+
return true;
221+
}
222+
223+
224+
#else // _WIN32
225+
226+
/////////////////////////////////////////////////////////////////////////////////////
227+
// Unix-specific code
228+
/////////////////////////////////////////////////////////////////////////////////////
229+
void process_query_status_unix(stdlib_pid pid, bool wait, bool* is_running, int* exit_code)
230+
{
231+
int status;
232+
int wait_code;
233+
234+
// Wait or return immediately if no status change
235+
int options = wait ? 0 : WNOHANG;
236+
237+
// Call waitpid to check the process state
238+
wait_code = waitpid(pid, &status, options);
239+
240+
if (wait_code > 0) {
241+
// Process state was updated
242+
if (WIFEXITED(status)) {
243+
*is_running = false;
244+
245+
// Get exit code
246+
*exit_code = WEXITSTATUS(status);
247+
} else if (WIFSIGNALED(status)) {
248+
*is_running = false;
249+
250+
// Use negative value to indicate termination by signal
251+
*exit_code = -WTERMSIG(status);
252+
} else {
253+
// Process is still running: no valid exit code yet
254+
*is_running = true;
255+
*exit_code = 0;
256+
}
257+
} else if (wait_code == 0) {
258+
// No status change; process is still running
259+
*is_running = true;
260+
*exit_code = 0;
261+
} else {
262+
// Error occurred
263+
*is_running = false;
264+
*exit_code = -1; // Indicate an error
265+
}
266+
}
267+
268+
// Kill a process by sending a SIGKILL signal. Return .true. if succeeded, or false if not.
269+
// Killing process may fail due to unexistent process, or not enough rights to kill.
270+
bool process_kill_unix(stdlib_pid pid) {
271+
// Send the SIGKILL signal to the process
272+
if (kill(pid, SIGKILL) == 0) {
273+
// Successfully sent the signal
274+
return true;
275+
}
276+
277+
// If `kill` fails, check if the process no longer exists
278+
if (errno == ESRCH) {
279+
// Process does not exist
280+
return true; // Already "terminated"
281+
}
282+
283+
// Other errors occurred
284+
return false;
285+
}
286+
287+
288+
// On UNIX systems: just fork a new process. The command line will be executed from Fortran.
289+
void process_create_posix(stdlib_pid* pid)
290+
{
291+
292+
(*pid) = (stdlib_pid) fork();
293+
}
294+
295+
#endif // _WIN32
296+
297+
/////////////////////////////////////////////////////////////////////////////////////
298+
// Cross-platform interface
299+
/////////////////////////////////////////////////////////////////////////////////////
300+
301+
// Create or fork process
302+
void process_create(const char* cmd, const char* stdin_stream, const char* stdin_file,
303+
const char* stdout_file, const char* stderr_file,
304+
stdlib_pid* pid) {
305+
#ifdef _WIN32
306+
process_create_windows(cmd, stdin_stream, stdin_file, stdout_file, stderr_file, pid);
307+
#else
308+
process_create_posix(pid);
309+
#endif // _WIN32
310+
}
311+
312+
// Cross-platform interface: query process state
313+
void process_query_status(stdlib_pid pid, bool wait, bool* is_running, int* exit_code)
314+
{
315+
#ifdef _WIN32
316+
process_query_status_windows(pid, wait, is_running, exit_code);
317+
#else
318+
process_query_status_unix (pid, wait, is_running, exit_code);
319+
#endif // _WIN32
320+
}
321+
322+
// Cross-platform interface: kill process by ID
323+
bool process_kill(stdlib_pid pid)
324+
{
325+
#ifdef _WIN32
326+
return process_kill_windows(pid);
327+
#else
328+
return process_kill_unix(pid);
329+
#endif // _WIN32
330+
}
331+
332+
// Cross-platform interface: sleep(seconds)
333+
void process_wait(float seconds)
334+
{
335+
#ifdef _WIN32
336+
DWORD dwMilliseconds = (DWORD) (seconds * 1000);
337+
Sleep(dwMilliseconds);
338+
#else
339+
int ierr;
340+
341+
unsigned int ms = (unsigned int) (seconds * 1000);
342+
struct timespec ts_remaining =
343+
{
344+
ms / 1000,
345+
(ms % 1000) * 1000000L
346+
};
347+
348+
do
349+
{
350+
struct timespec ts_sleep = ts_remaining;
351+
ierr = nanosleep(&ts_sleep, &ts_remaining);
352+
}
353+
while ((EINTR == errno) && (-1 == ierr));
354+
355+
if (ierr != 0){
356+
switch(errno){
357+
case EINTR:
358+
fprintf(stderr, "nanosleep() interrupted\n");
359+
break;
360+
case EINVAL:
361+
fprintf(stderr, "nanosleep() bad milliseconds value\n");
362+
exit(EINVAL);
363+
case EFAULT:
364+
fprintf(stderr, "nanosleep() problem copying information to user space\n");
365+
exit(EFAULT);
366+
case ENOSYS:
367+
fprintf(stderr, "nanosleep() not supported on this system\n");
368+
exit(ENOSYS);
369+
default:
370+
fprintf(stderr, "nanosleep() error\n");
371+
exit(1);
372+
}
373+
}
374+
375+
#endif // _WIN32
376+
}
377+
378+
// Returns the cross-platform file path of the null device for the current operating system.
379+
const char* process_null_device(int* len)
380+
{
381+
#ifdef _WIN32
382+
(*len) = strlen("NUL");
383+
return "NUL";
384+
#else
385+
(*len) = strlen("/dev/null");
386+
return "/dev/null";
387+
#endif
388+
}
389+
390+
// Returns a boolean flag if macro _WIN32 is defined
391+
bool process_is_windows()
392+
{
393+
#ifdef _WIN32
394+
return true;
395+
#else
396+
return false;
397+
#endif // _WIN32
398+
}
399+

‎test/system/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
ADDTEST(sleep)
2+
ADDTEST(subprocess)

‎test/system/test_subprocess.f90

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
module test_subprocess
2+
use testdrive, only : new_unittest, unittest_type, error_type, check, skip_test
3+
use stdlib_system, only: process_type, run, runasync, is_running, wait, update, elapsed, is_windows, kill
4+
5+
implicit none
6+
7+
contains
8+
9+
!> Collect all exported unit tests
10+
subroutine collect_suite(testsuite)
11+
!> Collection of tests
12+
type(unittest_type), allocatable, intent(out) :: testsuite(:)
13+
14+
testsuite = [ &
15+
new_unittest('test_run_synchronous', test_run_synchronous), &
16+
new_unittest('test_run_asynchronous', test_run_asynchronous), &
17+
new_unittest('test_process_kill', test_process_kill), &
18+
new_unittest('test_process_state', test_process_state) &
19+
]
20+
end subroutine collect_suite
21+
22+
!> Test running a synchronous process
23+
subroutine test_run_synchronous(error)
24+
type(error_type), allocatable, intent(out) :: error
25+
type(process_type) :: process
26+
character(len=*), parameter :: command = "echo Hello"
27+
28+
process = run(command, want_stdout=.true.)
29+
call check(error, process%completed)
30+
if (allocated(error)) return
31+
32+
call check(error, trim(process%stdout) == "Hello", "stdout=<"//trim(process%stdout)//">, expected <Hello>")
33+
end subroutine test_run_synchronous
34+
35+
!> Test running an asynchronous process
36+
subroutine test_run_asynchronous(error)
37+
type(error_type), allocatable, intent(out) :: error
38+
type(process_type) :: process
39+
logical :: running
40+
41+
! The closest possible to a cross-platform command that waits
42+
if (is_windows()) then
43+
process = runasync("ping -n 2 127.0.0.1")
44+
else
45+
process = runasync("ping -c 2 127.0.0.1")
46+
endif
47+
! Should not be immediately completed
48+
call check(error, .not. process%completed, "ping process should not complete immediately")
49+
if (allocated(error)) return
50+
51+
running = is_running(process)
52+
call check(error, running, "ping process should still be running immediately after started")
53+
if (allocated(error)) return
54+
55+
call wait(process)
56+
call check(error, process%completed, "process should be complete after `call wait`")
57+
if (allocated(error)) return
58+
59+
call check(error, elapsed(process)>1.0e-4, "There should be a non-zero elapsed time")
60+
61+
end subroutine test_run_asynchronous
62+
63+
!> Test killing an asynchronous process
64+
subroutine test_process_kill(error)
65+
type(error_type), allocatable, intent(out) :: error
66+
type(process_type) :: process
67+
logical :: running, success
68+
69+
! Start a long-running process asynchronously
70+
if (is_windows()) then
71+
process = runasync("ping -n 10 127.0.0.1")
72+
else
73+
process = runasync("ping -c 10 127.0.0.1")
74+
endif
75+
76+
! Ensure the process starts running
77+
call check(error, .not. process%completed, "Process should not be completed immediately after starting")
78+
if (allocated(error)) return
79+
80+
running = is_running(process)
81+
call check(error, running, "Process should be running immediately after starting")
82+
if (allocated(error)) return
83+
84+
! Kill the process
85+
call kill(process, success)
86+
call check(error, success, "Failed to kill the process")
87+
if (allocated(error)) return
88+
89+
! Verify the process is no longer running
90+
call check(error, .not. is_running(process), "Process should not be running after being killed")
91+
if (allocated(error)) return
92+
93+
! Ensure process state updates correctly after killing
94+
call check(error, process%completed, "Process should be marked as completed after being killed")
95+
end subroutine test_process_kill
96+
97+
!> Test updating and checking process state
98+
subroutine test_process_state(error)
99+
type(error_type), allocatable, intent(out) :: error
100+
type(process_type) :: process
101+
character(len=*), parameter :: command = "echo Testing"
102+
103+
process = run(command, want_stdout=.true., want_stderr=.true.)
104+
105+
call update(process)
106+
call check(error, process%completed)
107+
if (allocated(error)) return
108+
109+
call check(error, process%exit_code == 0, "Check zero exit code")
110+
if (allocated(error)) return
111+
112+
call check(error, len_trim(process%stderr) == 0, "Check no stderr output")
113+
if (allocated(error)) return
114+
115+
call check(error, trim(process%stdout) == "Testing", "stdout=<"//trim(process%stdout)//">, expected <Testing>")
116+
if (allocated(error)) return
117+
end subroutine test_process_state
118+
119+
end module test_subprocess
120+
121+
program tester
122+
use, intrinsic :: iso_fortran_env, only : error_unit
123+
use testdrive, only : run_testsuite, new_testsuite, testsuite_type
124+
use test_subprocess, only : collect_suite
125+
126+
implicit none
127+
128+
integer :: stat, is
129+
type(testsuite_type), allocatable :: testsuites(:)
130+
character(len=*), parameter :: fmt = '("#", *(1x, a))'
131+
132+
stat = 0
133+
134+
testsuites = [ &
135+
new_testsuite("subprocess", collect_suite) &
136+
]
137+
138+
do is = 1, size(testsuites)
139+
write(error_unit, fmt) "Testing:", testsuites(is)%name
140+
call run_testsuite(testsuites(is)%collect, error_unit, stat)
141+
end do
142+
143+
if (stat > 0) then
144+
write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
145+
error stop
146+
end if
147+
end program

0 commit comments

Comments
 (0)
Please sign in to comment.