| 1 | ! ============================================================================== |
| 2 | ! Module: coprocess |
| 3 | ! Purpose: Coprocess management for bidirectional communication |
| 4 | ! ============================================================================== |
| 5 | module coprocess |
| 6 | use shell_types |
| 7 | use system_interface |
| 8 | use iso_c_binding, only: c_int |
| 9 | use iso_fortran_env, only: output_unit, error_unit |
| 10 | implicit none |
| 11 | private :: kill_c ! Make kill_c private to avoid conflicts |
| 12 | |
| 13 | ! Coprocess type |
| 14 | type :: coproc_t |
| 15 | character(len=256) :: name = '' |
| 16 | character(len=:), allocatable :: command |
| 17 | integer(c_pid_t) :: pid = 0 |
| 18 | integer :: read_fd = -1 ! Shell reads from coprocess |
| 19 | integer :: write_fd = -1 ! Shell writes to coprocess |
| 20 | logical :: active = .false. |
| 21 | logical :: eof_reached = .false. |
| 22 | end type coproc_t |
| 23 | |
| 24 | ! Global coprocess registry |
| 25 | type(coproc_t), save :: coprocs(10) |
| 26 | integer, save :: num_coprocs = 0 |
| 27 | |
| 28 | interface |
| 29 | function pipe_c(fds) bind(C, name="pipe") |
| 30 | import :: c_int |
| 31 | integer(c_int) :: pipe_c |
| 32 | integer(c_int), intent(out) :: fds(2) |
| 33 | end function |
| 34 | |
| 35 | function fork_c() bind(C, name="fork") result(pid) |
| 36 | import :: c_pid_t |
| 37 | integer(c_pid_t) :: pid |
| 38 | end function |
| 39 | |
| 40 | function dup2_c(oldfd, newfd) bind(C, name="dup2") result(ret) |
| 41 | import :: c_int |
| 42 | integer(c_int), value :: oldfd, newfd |
| 43 | integer(c_int) :: ret |
| 44 | end function |
| 45 | |
| 46 | function close_c(fd) bind(C, name="close") result(ret) |
| 47 | import :: c_int |
| 48 | integer(c_int), value :: fd |
| 49 | integer(c_int) :: ret |
| 50 | end function |
| 51 | |
| 52 | function waitpid_c(pid, status, options) bind(C, name="waitpid") result(ret) |
| 53 | import :: c_pid_t, c_int |
| 54 | integer(c_pid_t), value :: pid |
| 55 | integer(c_int), intent(out) :: status |
| 56 | integer(c_int), value :: options |
| 57 | integer(c_pid_t) :: ret |
| 58 | end function |
| 59 | |
| 60 | function read_c(fd, buf, count) bind(C, name="read") result(bytes_read) |
| 61 | import :: c_int, c_ptr, c_size_t |
| 62 | integer(c_int), value :: fd |
| 63 | type(c_ptr), value :: buf |
| 64 | integer(c_size_t), value :: count |
| 65 | integer(c_size_t) :: bytes_read |
| 66 | end function |
| 67 | |
| 68 | function write_c(fd, buf, count) bind(C, name="write") result(bytes_written) |
| 69 | import :: c_int, c_ptr, c_size_t |
| 70 | integer(c_int), value :: fd |
| 71 | type(c_ptr), value :: buf |
| 72 | integer(c_size_t), value :: count |
| 73 | integer(c_size_t) :: bytes_written |
| 74 | end function |
| 75 | |
| 76 | function kill_c(pid, sig) bind(C, name="kill") result(ret) |
| 77 | import :: c_pid_t, c_int |
| 78 | integer(c_pid_t), value :: pid |
| 79 | integer(c_int), value :: sig |
| 80 | integer(c_int) :: ret |
| 81 | end function |
| 82 | |
| 83 | function execlp_c(file, arg) bind(C, name="execlp") |
| 84 | import :: c_int, c_ptr |
| 85 | type(c_ptr), value :: file |
| 86 | type(c_ptr), value :: arg |
| 87 | integer(c_int) :: execlp_c |
| 88 | end function |
| 89 | end interface |
| 90 | |
| 91 | ! C system() binding (avoid duplicate - use from system_interface if available) |
| 92 | interface |
| 93 | function system_c(command) bind(C, name="system") |
| 94 | import :: c_int, c_char |
| 95 | character(kind=c_char), intent(in) :: command(*) |
| 96 | integer(c_int) :: system_c |
| 97 | end function |
| 98 | end interface |
| 99 | |
| 100 | integer, parameter :: SIGTERM = 15 |
| 101 | integer, parameter :: SIGKILL = 9 |
| 102 | |
| 103 | contains |
| 104 | |
| 105 | subroutine init_coprocess_registry() |
| 106 | integer :: i |
| 107 | do i = 1, size(coprocs) |
| 108 | coprocs(i)%command = '' |
| 109 | end do |
| 110 | end subroutine |
| 111 | |
| 112 | ! Start a coprocess with optional name |
| 113 | function start_coprocess(command, name, interactive) result(coproc_id) |
| 114 | character(len=*), intent(in) :: command |
| 115 | character(len=*), intent(in), optional :: name |
| 116 | logical, intent(in), optional :: interactive |
| 117 | integer :: coproc_id |
| 118 | |
| 119 | integer(c_int) :: pipe_to_child(2), pipe_from_child(2) |
| 120 | integer(c_pid_t) :: pid |
| 121 | integer :: i, ret |
| 122 | character(len=256) :: coproc_name |
| 123 | |
| 124 | coproc_id = -1 |
| 125 | |
| 126 | ! Find available slot |
| 127 | do i = 1, size(coprocs) |
| 128 | if (.not. coprocs(i)%active) then |
| 129 | coproc_id = i |
| 130 | exit |
| 131 | end if |
| 132 | end do |
| 133 | |
| 134 | if (coproc_id == -1) then |
| 135 | write(error_unit, '(a)') 'coprocess: maximum number of coprocesses reached' |
| 136 | return |
| 137 | end if |
| 138 | |
| 139 | ! Create pipes |
| 140 | ret = pipe_c(pipe_to_child) |
| 141 | if (ret /= 0) then |
| 142 | write(error_unit, '(a)') 'coprocess: failed to create pipe to child' |
| 143 | return |
| 144 | end if |
| 145 | |
| 146 | ret = pipe_c(pipe_from_child) |
| 147 | if (ret /= 0) then |
| 148 | write(error_unit, '(a)') 'coprocess: failed to create pipe from child' |
| 149 | ret = close_c(pipe_to_child(1)) |
| 150 | ret = close_c(pipe_to_child(2)) |
| 151 | return |
| 152 | end if |
| 153 | |
| 154 | ! Fork process |
| 155 | pid = fork_c() |
| 156 | |
| 157 | if (pid == 0) then |
| 158 | ! Child process |
| 159 | |
| 160 | ! Redirect stdin to read from parent |
| 161 | ret = dup2_c(pipe_to_child(1), 0) |
| 162 | ret = close_c(pipe_to_child(1)) |
| 163 | ret = close_c(pipe_to_child(2)) |
| 164 | |
| 165 | ! Redirect stdout to write to parent |
| 166 | ret = dup2_c(pipe_from_child(2), 1) |
| 167 | ret = close_c(pipe_from_child(1)) |
| 168 | ret = close_c(pipe_from_child(2)) |
| 169 | |
| 170 | ! Execute command using shell |
| 171 | call execute_command_in_shell(command) |
| 172 | |
| 173 | else if (pid > 0) then |
| 174 | ! Parent process |
| 175 | |
| 176 | ! Close child ends of pipes |
| 177 | ret = close_c(pipe_to_child(1)) |
| 178 | ret = close_c(pipe_from_child(2)) |
| 179 | |
| 180 | ! Set up coprocess structure |
| 181 | if (present(name)) then |
| 182 | coproc_name = name |
| 183 | else |
| 184 | write(coproc_name, '(a,I0)') 'COPROC', coproc_id |
| 185 | end if |
| 186 | |
| 187 | coprocs(coproc_id)%name = coproc_name |
| 188 | coprocs(coproc_id)%command = command |
| 189 | coprocs(coproc_id)%pid = pid |
| 190 | coprocs(coproc_id)%write_fd = pipe_to_child(2) ! Shell writes here |
| 191 | coprocs(coproc_id)%read_fd = pipe_from_child(1) ! Shell reads here |
| 192 | coprocs(coproc_id)%active = .true. |
| 193 | coprocs(coproc_id)%eof_reached = .false. |
| 194 | |
| 195 | num_coprocs = max(num_coprocs, coproc_id) |
| 196 | |
| 197 | if (present(interactive) .and. interactive) then |
| 198 | write(error_unit, '(a,a,a,I0)') '[', trim(coproc_name), '] ', pid |
| 199 | end if |
| 200 | |
| 201 | else |
| 202 | ! Fork failed |
| 203 | write(error_unit, '(a)') 'coprocess: fork failed' |
| 204 | ret = close_c(pipe_to_child(1)) |
| 205 | ret = close_c(pipe_to_child(2)) |
| 206 | ret = close_c(pipe_from_child(1)) |
| 207 | ret = close_c(pipe_from_child(2)) |
| 208 | coproc_id = -1 |
| 209 | end if |
| 210 | end function |
| 211 | |
| 212 | ! Write to coprocess |
| 213 | function write_to_coprocess(coproc_id, data) result(success) |
| 214 | integer, intent(in) :: coproc_id |
| 215 | character(len=*), intent(in) :: data |
| 216 | logical :: success |
| 217 | |
| 218 | character(kind=c_char), target :: c_data(len(data)+1) |
| 219 | integer(c_size_t) :: bytes_written |
| 220 | integer :: i |
| 221 | |
| 222 | success = .false. |
| 223 | |
| 224 | if (coproc_id < 1 .or. coproc_id > size(coprocs)) return |
| 225 | if (.not. coprocs(coproc_id)%active) return |
| 226 | if (coprocs(coproc_id)%write_fd < 0) return |
| 227 | |
| 228 | ! Convert to C string |
| 229 | do i = 1, len(data) |
| 230 | c_data(i) = data(i:i) |
| 231 | end do |
| 232 | c_data(len(data)+1) = c_null_char |
| 233 | |
| 234 | ! Write to coprocess stdin |
| 235 | bytes_written = write_c(coprocs(coproc_id)%write_fd, c_loc(c_data), int(len(data), c_size_t)) |
| 236 | |
| 237 | if (bytes_written > 0) then |
| 238 | success = .true. |
| 239 | else |
| 240 | write(error_unit, '(a,a)') 'coprocess: write failed to ', trim(coprocs(coproc_id)%name) |
| 241 | end if |
| 242 | end function |
| 243 | |
| 244 | ! Read from coprocess |
| 245 | function read_from_coprocess(coproc_id, timeout_ms) result(data) |
| 246 | integer, intent(in) :: coproc_id |
| 247 | integer, intent(in), optional :: timeout_ms |
| 248 | character(len=4096) :: data |
| 249 | |
| 250 | character(kind=c_char), target :: c_buffer(4096) |
| 251 | integer(c_size_t) :: bytes_read |
| 252 | integer :: i |
| 253 | |
| 254 | data = '' |
| 255 | if (.false. .and. present(timeout_ms)) print *, timeout_ms ! Silence unused warning |
| 256 | |
| 257 | if (coproc_id < 1 .or. coproc_id > size(coprocs)) return |
| 258 | if (.not. coprocs(coproc_id)%active) return |
| 259 | if (coprocs(coproc_id)%eof_reached) return |
| 260 | if (coprocs(coproc_id)%read_fd < 0) return |
| 261 | |
| 262 | ! Read from coprocess stdout |
| 263 | bytes_read = read_c(coprocs(coproc_id)%read_fd, c_loc(c_buffer), int(4096, c_size_t)) |
| 264 | |
| 265 | if (bytes_read > 0) then |
| 266 | ! Convert C buffer to Fortran string |
| 267 | do i = 1, int(bytes_read) |
| 268 | data(i:i) = c_buffer(i) |
| 269 | end do |
| 270 | else if (bytes_read == 0) then |
| 271 | ! EOF reached |
| 272 | coprocs(coproc_id)%eof_reached = .true. |
| 273 | else |
| 274 | ! Read error |
| 275 | write(error_unit, '(a,a)') 'coprocess: read failed from ', trim(coprocs(coproc_id)%name) |
| 276 | end if |
| 277 | end function |
| 278 | |
| 279 | ! Find coprocess by name |
| 280 | function find_coprocess(name) result(coproc_id) |
| 281 | character(len=*), intent(in) :: name |
| 282 | integer :: coproc_id |
| 283 | integer :: i |
| 284 | |
| 285 | coproc_id = -1 |
| 286 | |
| 287 | do i = 1, num_coprocs |
| 288 | if (coprocs(i)%active .and. trim(coprocs(i)%name) == trim(name)) then |
| 289 | coproc_id = i |
| 290 | exit |
| 291 | end if |
| 292 | end do |
| 293 | end function |
| 294 | |
| 295 | ! Kill and cleanup coprocess |
| 296 | subroutine kill_coprocess(coproc_id) |
| 297 | integer, intent(in) :: coproc_id |
| 298 | |
| 299 | integer :: ret, status |
| 300 | |
| 301 | if (coproc_id < 1 .or. coproc_id > size(coprocs)) return |
| 302 | if (.not. coprocs(coproc_id)%active) return |
| 303 | |
| 304 | ! Close file descriptors |
| 305 | if (coprocs(coproc_id)%read_fd >= 0) then |
| 306 | ret = close_c(coprocs(coproc_id)%read_fd) |
| 307 | end if |
| 308 | |
| 309 | if (coprocs(coproc_id)%write_fd >= 0) then |
| 310 | ret = close_c(coprocs(coproc_id)%write_fd) |
| 311 | end if |
| 312 | |
| 313 | ! Kill process if still running |
| 314 | if (coprocs(coproc_id)%pid > 0) then |
| 315 | ! Try SIGTERM first (graceful) |
| 316 | ret = kill_c(coprocs(coproc_id)%pid, SIGTERM) |
| 317 | ! Wait briefly and check if process is still alive |
| 318 | ret = waitpid_c(coprocs(coproc_id)%pid, status, 1) ! WNOHANG = 1 |
| 319 | if (ret == 0) then |
| 320 | ! Process still running, force kill |
| 321 | ret = kill_c(coprocs(coproc_id)%pid, SIGKILL) |
| 322 | ret = waitpid_c(coprocs(coproc_id)%pid, status, 0) |
| 323 | end if |
| 324 | end if |
| 325 | |
| 326 | ! Mark as inactive |
| 327 | coprocs(coproc_id)%active = .false. |
| 328 | coprocs(coproc_id)%name = '' |
| 329 | coprocs(coproc_id)%command = '' |
| 330 | coprocs(coproc_id)%pid = 0 |
| 331 | coprocs(coproc_id)%read_fd = -1 |
| 332 | coprocs(coproc_id)%write_fd = -1 |
| 333 | coprocs(coproc_id)%eof_reached = .false. |
| 334 | end subroutine |
| 335 | |
| 336 | ! List active coprocesses |
| 337 | subroutine list_coprocesses() |
| 338 | integer :: i |
| 339 | logical :: found_any |
| 340 | |
| 341 | found_any = .false. |
| 342 | |
| 343 | do i = 1, num_coprocs |
| 344 | if (coprocs(i)%active) then |
| 345 | if (.not. found_any) then |
| 346 | write(output_unit, '(a)') 'Active coprocesses:' |
| 347 | found_any = .true. |
| 348 | end if |
| 349 | write(output_unit, '(a,I2,a,a,a,I0,a,a)') '[', i, '] ', & |
| 350 | trim(coprocs(i)%name), ' PID:', coprocs(i)%pid, ' CMD: ', trim(coprocs(i)%command) |
| 351 | end if |
| 352 | end do |
| 353 | |
| 354 | if (.not. found_any) then |
| 355 | write(output_unit, '(a)') 'No active coprocesses' |
| 356 | end if |
| 357 | end subroutine |
| 358 | |
| 359 | ! Cleanup all coprocesses |
| 360 | subroutine cleanup_all_coprocesses() |
| 361 | integer :: i |
| 362 | |
| 363 | do i = 1, num_coprocs |
| 364 | if (coprocs(i)%active) then |
| 365 | call kill_coprocess(i) |
| 366 | end if |
| 367 | end do |
| 368 | |
| 369 | num_coprocs = 0 |
| 370 | end subroutine |
| 371 | |
| 372 | function int_to_string(val) result(str) |
| 373 | integer, intent(in) :: val |
| 374 | character(len=32) :: str |
| 375 | |
| 376 | write(str, '(I0)') val |
| 377 | end function |
| 378 | |
| 379 | subroutine execute_command_in_shell(command) |
| 380 | character(len=*), intent(in) :: command |
| 381 | |
| 382 | character(kind=c_char), target :: c_command(len(command)+1) |
| 383 | integer(c_int) :: exit_status |
| 384 | integer :: i |
| 385 | |
| 386 | ! Convert command to C string |
| 387 | do i = 1, len(command) |
| 388 | c_command(i) = command(i:i) |
| 389 | end do |
| 390 | c_command(len(command)+1) = c_null_char |
| 391 | |
| 392 | ! Execute command using system() |
| 393 | ! This runs the command in a subshell (via /bin/sh -c) |
| 394 | exit_status = system_c(c_command) |
| 395 | |
| 396 | ! Exit with the command's exit status (use c_exit from iso_c_binding) |
| 397 | call c_exit(int(exit_status / 256, c_int)) ! Extract exit code from wait status |
| 398 | end subroutine |
| 399 | |
| 400 | end module coprocess |