Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ src/
cabledyn_blas.c BLAS thread policy; run-time OpenBLAS loading (Windows GNU)
cabledyn_mutex.c C-ABI registry and initialisation locks
cabledyn_crt_locale.c MinGW-w64 guard for libgfortran's locale restore
CableDyn_FatalReport.f90 abnormal-end report of the driver (time reached)
cabledyn_fatal.c interrupt, fault and stack-overflow handlers of the driver

EI = 0 cable path:
CableDyn_Mesh.f90 connectivity, property, and DOF-partition validation
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ All notable changes to CableDyn are recorded in this file. The format follows

### Fixed

- The standalone driver no longer ends without a word when its process is interrupted or hits a
fatal fault. Ctrl+C, Ctrl+Break, closing the console window, `SIGINT`, `SIGTERM` and `SIGHUP`
are reported as `CableDyn_driver: stopped by <cause>`, and an access violation, a stack
overflow or another fatal fault as `CableDyn_driver: fatal error: <cause>`. Both reports give
the simulated time of the last committed step, and the exit status is unchanged. Previously a
stack overflow in a Windows GNU build ended the process with no message. Every thread that
enters an OpenMP parallel region keeps room to report its own stack overflow, and on Linux
and macOS a hardware fault reaches the handler that was there before (the Fortran runtime's
backtrace, or the default action and its core dump) with its original address and context,
and a previous handler runs under its own flags and signal mask.
A signal the driver was started with ignored (for example under `nohup`) stays ignored.
- Every non-zero exit the driver makes itself now ends stderr with the closing line
`CableDyn_driver: ended with exit code <n>`. A process ended from outside (`taskkill /F`,
*End task*, `kill -9`) runs none of its own code and cannot report anything; the missing
closing line now identifies it, since `taskkill /F` leaves exit code 1, the same code as a
refused input. `CableDynDriver.run`, `cabledyn-run` and `cabledyn-study` report such a run as
ended early, with the time its output reached, instead of relaying the start-up log as the
error. The documentation explains how to recognise each kind of early end and warns that
`taskkill /IM CableDyn_driver.exe /F` ends every CableDyn run on the computer.
- The static solve of a taut, neutrally buoyant finite-EI line (mass per length equal to
the displaced mass) no longer depends on the sign of the round-off weight: a line whose
total weight is below 1e-12 of its axial stiffness is seeded as a straight, uniformly
Expand Down
65 changes: 63 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ add_library(cabledyn_obj OBJECT
src/cabledyn_blas.c
src/cabledyn_crt_locale.c
src/cabledyn_path.c
src/cabledyn_fatal.c
src/CableDyn_Precision.f90
src/CableDyn_FatalReport.f90
src/CableDyn_PathIO.f90
src/CableDyn_Banner.f90
src/CableDyn_Linalg.f90
Expand Down Expand Up @@ -1319,6 +1321,13 @@ if(Python3_Interpreter_FOUND)
LABELS "python;documentation;examples"
ENVIRONMENT "${_cabledyn_python_test_environment}")

# Every OpenMP parallel region prepares its threads for the abnormal-end report.
add_test(NAME omp_fatal_init
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_omp_fatal_init.py)
set_tests_properties(omp_fatal_init PROPERTIES
LABELS "python;unit;stack"
ENVIRONMENT "${_cabledyn_python_test_environment}")

add_test(NAME documentation_sources
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_documentation.py)
set_tests_properties(documentation_sources PROPERTIES
Expand Down Expand Up @@ -1660,7 +1669,7 @@ target_link_libraries(test_stream_wave PRIVATE cabledyn_core)
add_test(NAME stream_wave COMMAND test_stream_wave)
set_tests_properties(stream_wave PROPERTIES LABELS "fortran;unit;driver;dynamic;validation;orcaflex")

add_executable(test_modal tests/test_modal.f90)
add_executable(test_modal tests/test_modal.f90 tests/blas_config_probe.c)
target_link_libraries(test_modal PRIVATE cabledyn_core)
add_test(NAME modal COMMAND test_modal)
set_tests_properties(modal PROPERTIES LABELS "fortran;unit;driver;static;validation")
Expand All @@ -1674,7 +1683,7 @@ set_tests_properties(modal PROPERTIES LABELS "fortran;unit;driver;static;validat
# the library on every platform.
if(CABLEDYN_GNU_WINDOWS_STACK)
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/stack_small")
add_executable(test_modal_small_stack tests/test_modal.f90)
add_executable(test_modal_small_stack tests/test_modal.f90 tests/blas_config_probe.c)
target_link_libraries(test_modal_small_stack PRIVATE cabledyn_core)
add_executable(cabledyn_small_stack app/cabledyn.f90)
target_link_libraries(cabledyn_small_stack PRIVATE cabledyn_core)
Expand Down Expand Up @@ -1807,6 +1816,58 @@ add_executable(test_crt_locale_guard tests/test_crt_locale_guard.f90 tests/crt_l
target_link_libraries(test_crt_locale_guard PRIVATE cabledyn_core)
add_test(NAME crt_locale_guard COMMAND test_crt_locale_guard $<IF:$<BOOL:${MINGW}>,1,0>)
set_tests_properties(crt_locale_guard PROPERTIES LABELS "fortran;unit")
# An abnormal end of the driver process is never silent: a stack overflow (on a 1 MiB stack
# in Windows GNU builds), an access violation and, off Windows, SIGTERM each end the process
# with a report on stderr of the cause and the simulated time reached; the march records the
# time of every committed step for that report.
add_executable(test_fatal_report tests/test_fatal_report.f90 tests/fatal_report_faults.c)
target_link_libraries(test_fatal_report PRIVATE cabledyn_core)
if(CABLEDYN_GNU_WINDOWS_STACK)
target_link_options(test_fatal_report PRIVATE "-Wl,--stack,${CABLEDYN_SMALL_STACK_RESERVE}")
endif()
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/fatal_report")
add_test(NAME fatal_report_march
COMMAND test_fatal_report march ${CMAKE_CURRENT_SOURCE_DIR}/examples/dynamic_chain_held.dat
${CMAKE_BINARY_DIR}/fatal_report/march)
set_tests_properties(fatal_report_march PROPERTIES LABELS "fortran;unit;driver;dynamic")
# The cause names the fault as each platform delivers it (macOS may deliver SIGBUS).
foreach(_fatal IN ITEMS "overflow|during|(stack overflow|segmentation fault|bus error)"
"overflow|before|(stack overflow|segmentation fault|bus error)"
"null|during|(access violation|segmentation fault|bus error)"
"term|during|a termination request")
string(REPLACE "|" ";" _fatal_case "${_fatal}")
list(GET _fatal_case 0 _fatal_mode)
list(GET _fatal_case 1 _fatal_when)
list(SUBLIST _fatal_case 2 -1 _fatal_cause)
string(JOIN "|" _fatal_cause ${_fatal_cause})
add_test(NAME fatal_report_${_fatal_mode}_${_fatal_when}
COMMAND ${CMAKE_COMMAND} -DEXE=$<TARGET_FILE:test_fatal_report> -DMODE=${_fatal_mode}
-DWHEN=${_fatal_when} "-DCAUSE=${_fatal_cause}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_fatal_report.cmake)
set_tests_properties(fatal_report_${_fatal_mode}_${_fatal_when} PROPERTIES
LABELS "fortran;unit;driver;stack" TIMEOUT 120)
endforeach()
# A stack overflow on an OpenMP worker thread with a 1 MiB stack is reported too: every
# thread that enters a parallel region gets room for the handlers (tests/test_omp_fatal_init.py
# checks that every region of the library and the driver asks for it).
add_test(NAME fatal_report_omp_overflow_during
COMMAND ${CMAKE_COMMAND} -DEXE=$<TARGET_FILE:test_fatal_report> -DMODE=omp_overflow
-DWHEN=during "-DCAUSE=(stack overflow|segmentation fault|bus error)"
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_fatal_report.cmake)
set_tests_properties(fatal_report_omp_overflow_during PROPERTIES
LABELS "fortran;unit;driver;stack" TIMEOUT 120
ENVIRONMENT "OMP_NUM_THREADS=2;OMP_STACKSIZE=1M")
# POSIX: a hardware fault keeps its own siginfo and context through the report.
add_test(NAME fatal_report_fault_context COMMAND test_fatal_report context)
set_tests_properties(fatal_report_fault_context PROPERTIES
LABELS "fortran;unit;driver;stack" TIMEOUT 120
FAIL_REGULAR_EXPRESSION "FAIL:")
# POSIX: the report keeps every previous disposition -- an ignored signal (with or without
# SA_SIGINFO) stays ignored, a default one still ends the process, a handler still runs.
add_test(NAME fatal_report_dispositions COMMAND test_fatal_report dispositions)
set_tests_properties(fatal_report_dispositions PROPERTIES
LABELS "fortran;unit;driver" TIMEOUT 120
FAIL_REGULAR_EXPRESSION "FAIL:")
add_test(
NAME driver_repeat_exit
COMMAND ${CMAKE_COMMAND}
Expand Down
52 changes: 37 additions & 15 deletions app/cabledyn.f90
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ PROGRAM cabledyn
!! BODIES and RODS -- and, when dtM/TMax are present, marches the dynamics while
!! writing the requested OUTPUTS at each row (doc/cli.rst).
!!
!! Exit codes: 0 converged, 1 bad/unparseable deck, 2 solve failure.
!! Exit codes: 0 converged, 1 bad/unparseable deck, 2 solve failure. Every non-zero exit
!! the driver makes itself ends stderr with the closing line "CableDyn_driver: ended with
!! exit code <n>" (stop_run); an interrupt or a fatal fault is reported by
!! CableDyn_FatalReport with the simulated time reached.
USE CableDyn_Precision, ONLY: wp, CD_ZERO
USE CableDyn_DeckDriver, ONLY: CD_Run_Deck_Driver, CD_Deck_Query_dtM, &
CD_Classify_Dynamic_Completion, CD_DECKDRV_OK, &
Expand All @@ -30,6 +33,7 @@ PROGRAM cabledyn
CD_AGG_Range_Sample, CD_AGG_Range_Write, CD_AGG_OK, CD_AGG_BADINPUT
USE CableDyn_Banner, ONLY: CD_Print_Banner
USE CableDyn_Linalg, ONLY: CD_Blas_Runtime_Check, CD_LINALG_OK
USE CableDyn_FatalReport, ONLY: CD_Fatal_Report_Install, CD_Fatal_Report_Time
USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: error_unit, output_unit, int64
IMPLICIT NONE
TYPE :: StandaloneProgress
Expand Down Expand Up @@ -78,14 +82,14 @@ PROGRAM cabledyn
IF (deck_path(1:1) == '-') THEN
WRITE (error_unit, '(A)') PROG//': unknown option "'//TRIM(deck_path)//'"'
CALL print_usage(error_unit)
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
END DO

IF (nargs /= 2) THEN
CALL CD_Print_Banner(error_unit)
CALL print_usage(error_unit)
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
CALL get_argument(1, deck_path)
CALL get_argument(2, out_root)
Expand All @@ -101,8 +105,13 @@ PROGRAM cabledyn
CALL CD_Blas_Runtime_Check(PROG, ErrStat, ErrMsg)
IF (ErrStat /= CD_LINALG_OK) THEN
WRITE (error_unit, '(A)') TRIM(ErrMsg)
STOP 2, QUIET = .TRUE.
CALL stop_run(2)
END IF
! From here on an interrupt or a fatal fault (an access violation, a stack overflow) is
! reported on stderr with the simulated time reached, instead of ending the process
! without a word. Installed after the LAPACK runtime has loaded, so its handlers are the
! process's own.
CALL CD_Fatal_Report_Install()
CALL CD_Deck_Query_dtM(TRIM(deck_path), dtM, has_dtm, ErrStat, ErrMsg, tmax=tmax, &
has_tmax=has_tmax, n_ei0=n_ei0, n_finite_ei=n_finite_ei, &
has_motion_file=has_motion_file, standalone_scan=.TRUE., &
Expand All @@ -125,9 +134,9 @@ PROGRAM cabledyn
IF (ErrStat /= CD_DECKDRV_OK) THEN
WRITE (error_unit, '(A)') TRIM(ErrMsg)
IF (ErrStat == CD_DECKDRV_BADINPUT) THEN
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
ELSE
STOP 2, QUIET = .TRUE.
CALL stop_run(2)
END IF
END IF
! Keep the process-level contract fail-closed even if a future driver route
Expand All @@ -138,12 +147,23 @@ PROGRAM cabledyn
ELSE
WRITE (error_unit, '(A)') 'CableDyn_driver: run did not converge; output is for inspection only.'
END IF
STOP 2, QUIET = .TRUE.
CALL stop_run(2)
END IF
WRITE (output_unit, '(A,A,A)') PROG//': converged run written to ', TRIM(out_root), '.out'

CONTAINS

SUBROUTINE stop_run(code)
!! End the process with a non-zero exit code, after the diagnostic already written.
!! The closing line is the last stderr record of every exit the driver makes itself, so
!! a caller can tell such an exit from a process ended from outside (Task Manager,
!! `taskkill /F`, `kill -9`), which runs no code of the driver and leaves no closing line.
INTEGER, INTENT(IN) :: code
WRITE (error_unit, '(A,I0)') PROG//': ended with exit code ', code
FLUSH (error_unit)
STOP code, QUIET = .TRUE.
END SUBROUTINE stop_run

SUBROUTINE get_argument(index, value)
!! Command-line argument that fails closed when it cannot be retrieved whole
!! (a path longer than the buffer would otherwise be silently truncated).
Expand All @@ -155,14 +175,14 @@ SUBROUTINE get_argument(index, value)
IF (arg_stat == -1) THEN
WRITE (error_unit, '(A,I0,A,I0,A)') PROG//': argument ', index, ' is longer than ', LEN(value), &
' characters'
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
ELSE IF (arg_stat /= 0) THEN
WRITE (error_unit, '(A,I0)') PROG//': cannot read command-line argument ', index
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
IF (arg_len < 1) THEN
WRITE (error_unit, '(A,I0,A)') PROG//': command-line argument ', index, ' is empty'
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
END SUBROUTINE get_argument

Expand All @@ -180,12 +200,12 @@ SUBROUTINE prepare_paths(deck, root, deck_spelling, root_spelling)
CALL CD_Native_Path(deck, deck_spelling, stat, why)
IF (stat /= CD_PATH_OK) THEN
WRITE (error_unit, '(A)') PROG//': cannot read deck "'//deck//'": '//TRIM(why)
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
CALL CD_Native_Path(root, root_spelling, stat, why, for_output=.TRUE., reserve=MAX_OUTPUT_SUFFIX)
IF (stat /= CD_PATH_OK) THEN
WRITE (error_unit, '(A)') PROG//': cannot write output files at "'//root//'": '//TRIM(why)
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
END SUBROUTINE prepare_paths

Expand All @@ -206,7 +226,7 @@ SUBROUTINE check_output_root(deck, deck_spelling, root, root_spelling)
IF (ios /= 0) THEN
WRITE (error_unit, '(A)') PROG//': cannot write output files at "'//root// &
'" (check that the directory exists and is writable)'
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
IF (exists) THEN
CLOSE (unit)
Expand Down Expand Up @@ -265,7 +285,7 @@ SUBROUTINE check_input_not_output(input, root, root_spelling, what, input_spelli
WRITE (error_unit, '(A)') PROG//': output root "'//root//'" would overwrite the '//what//' "'// &
input//'" that the deck reads'
END IF
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END IF
END SUBROUTINE check_input_not_output

Expand Down Expand Up @@ -337,7 +357,7 @@ SUBROUTINE lock_output_root(root)
CASE DEFAULT
WRITE (error_unit, '(A)') PROG//': cannot create the output lock file "'//root//LOCK_SUFFIX//'"'
END SELECT
STOP 1, QUIET = .TRUE.
CALL stop_run(1)
END SUBROUTINE lock_output_root

LOGICAL FUNCTION same_file(a, b) RESULT(same)
Expand Down Expand Up @@ -773,6 +793,8 @@ SUBROUTINE update_standalone_progress(progress, step, simulated_time)
INTEGER(int64) :: now
REAL(wp) :: elapsed, remaining, percent
CHARACTER(16) :: elapsed_text, remaining_text
! Every committed step: the time an abnormal end of the process reports.
CALL CD_Fatal_Report_Time(simulated_time)
IF (step < progress%nstep .AND. MOD(step, progress%stride) /= 0) RETURN
CALL SYSTEM_CLOCK(now)
elapsed = CD_ZERO
Expand Down
4 changes: 4 additions & 0 deletions doc/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ The exchange contract is in {doc}`coupling_boundary`.
- modal analysis about the static equilibrium (`nModes`, `<root>.modes.out`)
* - `CableDyn_PathIO`
- UTF-8 file names converted to the spelling the Fortran runtime opens exactly
* - `CableDyn_FatalReport`
- the driver's report of an interrupt or a fatal fault, with the simulated time of the last committed step
```

## C helper sources
Expand All @@ -194,4 +196,6 @@ The exchange contract is in {doc}`coupling_boundary`.
- process-local locks of the C ABI (handle registry and deck initialisation)
* - `cabledyn_crt_locale.c`
- a guard around the numeric-locale handling of the MinGW-w64 Fortran runtime
* - `cabledyn_fatal.c`
- the driver's report of an interrupt or a fatal fault with the simulated time reached (the counterpart of `CableDyn_FatalReport`)
```
7 changes: 5 additions & 2 deletions doc/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,11 @@ Exit codes
message names the library, each location tried and the Windows load error. The release
``CableDyn_driver.exe`` is statically linked and has no such dependency

Every non-zero exit is accompanied by a message on stderr. See :doc:`troubleshooting` for the
messages and their remedies.
Every non-zero exit the driver makes itself writes its message on stderr and ends stderr with
the closing line ``CableDyn_driver: ended with exit code <n>``. An interrupt or a fatal fault is
reported with the simulated time reached; a process ended from outside (``taskkill /F``,
``kill -9``) cannot report anything. See :ref:`run-ended-early` for how to recognise each case,
and :doc:`troubleshooting` for the messages and their remedies.

Examples
~~~~~~~~
Expand Down
Loading
Loading