Fortran · 32428 bytes Raw Blame History
1 module fgof_devloop
2 use fgof_jobs, only : &
3 attach_job, &
4 attach_pipeline_members, &
5 clear_job_handle, &
6 clear_job_spec, &
7 configure_job, &
8 FGOF_JOBS_SIGNAL_SCOPE_GROUP, &
9 FGOF_JOBS_TERMINAL_HANDOFF_FOREGROUND, &
10 job_continue_result, &
11 job_exit_result, &
12 job_handle, &
13 job_is_configured, &
14 job_is_finished, &
15 job_is_running, &
16 job_is_stopped, &
17 job_needs_cleanup, &
18 job_owns_process_group, &
19 job_requires_terminal_handoff, &
20 job_result, &
21 job_signal_result, &
22 job_signal_scope, &
23 job_spec, &
24 job_stop_result, &
25 make_job_spec, &
26 observe_wait_result, &
27 release_job
28 use fgof_process, only : &
29 FGOF_PROCESS_OK, &
30 process_command, &
31 process_options, &
32 process_result, &
33 run_process => run
34 use fgof_watch_types, only : &
35 FGOF_WATCH_EVT_CREATED, &
36 FGOF_WATCH_EVT_MODIFIED, &
37 FGOF_WATCH_EVT_MOVED, &
38 FGOF_WATCH_EVT_NONE, &
39 FGOF_WATCH_EVT_REMOVED, &
40 watch_event, &
41 watch_options
42 use fgof_devloop_types, only : &
43 FGOF_DEVLOOP_DECISION_IDLE, &
44 FGOF_DEVLOOP_DECISION_RESTART, &
45 FGOF_DEVLOOP_DECISION_RUN, &
46 FGOF_DEVLOOP_DECISION_STOP, &
47 FGOF_DEVLOOP_COMMAND_BUILD, &
48 FGOF_DEVLOOP_COMMAND_NONE, &
49 FGOF_DEVLOOP_COMMAND_RUN, &
50 FGOF_DEVLOOP_COMMAND_SMOKE, &
51 FGOF_DEVLOOP_JOB_ACTION_NONE, &
52 FGOF_DEVLOOP_JOB_ACTION_RESTART, &
53 FGOF_DEVLOOP_JOB_ACTION_START, &
54 FGOF_DEVLOOP_JOB_ACTION_STOP, &
55 FGOF_DEVLOOP_TRIGGER_CHANGE, &
56 FGOF_DEVLOOP_TRIGGER_MANUAL, &
57 FGOF_DEVLOOP_TRIGGER_NONE, &
58 FGOF_DEVLOOP_TRIGGER_START, &
59 devloop_command_result, &
60 devloop_command_spec, &
61 devloop_cycle, &
62 devloop_decision, &
63 devloop_job_plan, &
64 devloop_job_spec, &
65 devloop_job_state, &
66 devloop_options, &
67 devloop_state, &
68 devloop_supervision_result, &
69 devloop_trigger, &
70 devloop_watch_summary
71 implicit none
72 private
73
74 character(len=*), parameter :: FGOF_DEVLOOP_BACKEND_MODEL = "model"
75
76 public :: begin_devloop_cycle
77 public :: attach_devloop_job
78 public :: attach_devloop_pipeline_members
79 public :: clear_devloop_command_result
80 public :: clear_devloop_command_spec
81 public :: clear_devloop_cycle
82 public :: clear_devloop_decision
83 public :: clear_devloop_job_plan
84 public :: clear_devloop_job_spec
85 public :: clear_devloop_job_state
86 public :: clear_devloop_options
87 public :: clear_devloop_state
88 public :: clear_devloop_supervision_result
89 public :: clear_devloop_trigger
90 public :: clear_devloop_watch_summary
91 public :: devloop_backend_name
92 public :: devloop_build_command
93 public :: devloop_change_trigger
94 public :: devloop_command_result
95 public :: devloop_command_spec
96 public :: devloop_cycle
97 public :: devloop_decision
98 public :: devloop_job_plan
99 public :: devloop_job_restart_plan
100 public :: devloop_job_spec
101 public :: devloop_job_state
102 public :: devloop_manual_trigger
103 public :: devloop_options
104 public :: devloop_run_command
105 public :: devloop_service_job
106 public :: devloop_smoke_command
107 public :: devloop_start_trigger
108 public :: devloop_state
109 public :: devloop_supervision_result
110 public :: devloop_summarize_watch_events
111 public :: devloop_trigger
112 public :: devloop_watch_failure_summary
113 public :: devloop_watch_options
114 public :: devloop_watch_summary
115 public :: devloop_watch_trigger
116 public :: finish_devloop_cycle
117 public :: FGOF_DEVLOOP_DECISION_IDLE
118 public :: FGOF_DEVLOOP_DECISION_RESTART
119 public :: FGOF_DEVLOOP_DECISION_RUN
120 public :: FGOF_DEVLOOP_DECISION_STOP
121 public :: FGOF_DEVLOOP_COMMAND_BUILD
122 public :: FGOF_DEVLOOP_COMMAND_NONE
123 public :: FGOF_DEVLOOP_COMMAND_RUN
124 public :: FGOF_DEVLOOP_COMMAND_SMOKE
125 public :: FGOF_DEVLOOP_JOB_ACTION_NONE
126 public :: FGOF_DEVLOOP_JOB_ACTION_RESTART
127 public :: FGOF_DEVLOOP_JOB_ACTION_START
128 public :: FGOF_DEVLOOP_JOB_ACTION_STOP
129 public :: FGOF_DEVLOOP_TRIGGER_CHANGE
130 public :: FGOF_DEVLOOP_TRIGGER_MANUAL
131 public :: FGOF_DEVLOOP_TRIGGER_NONE
132 public :: FGOF_DEVLOOP_TRIGGER_START
133 public :: job_continue_result
134 public :: job_exit_result
135 public :: job_handle
136 public :: job_result
137 public :: job_signal_result
138 public :: job_spec
139 public :: job_stop_result
140 public :: observe_devloop_job
141 public :: release_devloop_job
142 public :: run_devloop_command
143 public :: run_devloop_cycle
144 public :: should_start_on_open
145 public :: start_devloop
146 public :: stop_devloop
147
148 contains
149
150 function clear_devloop_options() result(options)
151 type(devloop_options) :: options
152
153 options%run_on_start = .true.
154 options%restart_on_change = .true.
155 options%restart_on_directory_change = .true.
156 options%ignore_hidden = .false.
157 options%stop_on_failure = .false.
158 options%max_failures = 0
159 options%min_restart_changes = 1
160 options%debounce_polls = 0
161 end function clear_devloop_options
162
163 function clear_devloop_trigger() result(trigger)
164 type(devloop_trigger) :: trigger
165
166 trigger%kind = FGOF_DEVLOOP_TRIGGER_NONE
167 trigger%change_count = 0
168 trigger%reason = ""
169 end function clear_devloop_trigger
170
171 function clear_devloop_cycle() result(cycle)
172 type(devloop_cycle) :: cycle
173
174 cycle%id = 0
175 cycle%trigger_kind = FGOF_DEVLOOP_TRIGGER_NONE
176 cycle%change_count = 0
177 cycle%exit_code = 0
178 cycle%started = .false.
179 cycle%finished = .false.
180 cycle%succeeded = .false.
181 cycle%reason = ""
182 end function clear_devloop_cycle
183
184 function clear_devloop_decision() result(decision)
185 type(devloop_decision) :: decision
186
187 decision%kind = FGOF_DEVLOOP_DECISION_IDLE
188 decision%cycle_id = 0
189 decision%failure_count = 0
190 decision%should_run = .false.
191 decision%should_stop = .false.
192 decision%reason = ""
193 end function clear_devloop_decision
194
195 function clear_devloop_watch_summary() result(summary)
196 type(devloop_watch_summary) :: summary
197
198 summary%event_count = 0
199 summary%change_count = 0
200 summary%file_change_count = 0
201 summary%directory_change_count = 0
202 summary%created_count = 0
203 summary%modified_count = 0
204 summary%removed_count = 0
205 summary%moved_count = 0
206 summary%ignored_none_count = 0
207 summary%watch_error_code = 0
208 summary%has_changes = .false.
209 summary%watch_failed = .false.
210 summary%watch_error_message = ""
211 end function clear_devloop_watch_summary
212
213 function clear_devloop_command_spec() result(spec)
214 type(devloop_command_spec) :: spec
215
216 spec%kind = FGOF_DEVLOOP_COMMAND_NONE
217 spec%enabled = .false.
218 spec%options = process_options()
219 spec%label = ""
220 end function clear_devloop_command_spec
221
222 function clear_devloop_command_result() result(command_result)
223 type(devloop_command_result) :: command_result
224
225 command_result%kind = FGOF_DEVLOOP_COMMAND_NONE
226 command_result%requested = .false.
227 command_result%skipped = .true.
228 command_result%launched = .false.
229 command_result%completed = .false.
230 command_result%succeeded = .false.
231 command_result%timed_out = .false.
232 command_result%exit_code = 0
233 command_result%process_error_code = FGOF_PROCESS_OK
234 command_result%process = clear_process_result()
235 command_result%label = ""
236 command_result%error_message = ""
237 end function clear_devloop_command_result
238
239 function clear_devloop_supervision_result() result(supervision)
240 type(devloop_supervision_result) :: supervision
241
242 supervision%cycle = clear_devloop_cycle()
243 supervision%decision = clear_devloop_decision()
244 supervision%build = clear_devloop_command_result()
245 supervision%run = clear_devloop_command_result()
246 supervision%smoke = clear_devloop_command_result()
247 supervision%command_count = 0
248 supervision%failed_command_kind = FGOF_DEVLOOP_COMMAND_NONE
249 supervision%last_exit_code = 0
250 supervision%process_error_code = FGOF_PROCESS_OK
251 supervision%started = .false.
252 supervision%succeeded = .false.
253 supervision%failed = .false.
254 supervision%timed_out = .false.
255 end function clear_devloop_supervision_result
256
257 function clear_devloop_job_spec() result(spec)
258 type(devloop_job_spec) :: spec
259
260 spec%job = clear_job_spec()
261 spec%enabled = .false.
262 spec%stop_before_restart = .true.
263 spec%release_on_handoff = .false.
264 spec%label = ""
265 end function clear_devloop_job_spec
266
267 function clear_devloop_job_state() result(job_state)
268 type(devloop_job_state) :: job_state
269
270 job_state%spec = clear_devloop_job_spec()
271 job_state%handle = clear_job_handle()
272 job_state%pid = 0
273 job_state%process_group = 0
274 job_state%signal_scope = FGOF_JOBS_SIGNAL_SCOPE_GROUP
275 job_state%configured = .false.
276 job_state%attached = .false.
277 job_state%running = .false.
278 job_state%stopped = .false.
279 job_state%finished = .false.
280 job_state%cleanup_needed = .false.
281 job_state%owns_process_group = .false.
282 job_state%terminal_handoff_required = .false.
283 job_state%released = .false.
284 job_state%label = ""
285 end function clear_devloop_job_state
286
287 function clear_devloop_job_plan() result(plan)
288 type(devloop_job_plan) :: plan
289
290 plan%action = FGOF_DEVLOOP_JOB_ACTION_NONE
291 plan%pid = 0
292 plan%process_group = 0
293 plan%signal_scope = FGOF_JOBS_SIGNAL_SCOPE_GROUP
294 plan%should_start = .false.
295 plan%should_stop = .false.
296 plan%should_restart = .false.
297 plan%should_release = .false.
298 plan%cleanup_needed = .false.
299 plan%terminal_handoff_required = .false.
300 plan%reason = ""
301 end function clear_devloop_job_plan
302
303 function clear_devloop_state() result(state)
304 type(devloop_state) :: state
305
306 state%options = clear_devloop_options()
307 state%last_cycle = clear_devloop_cycle()
308 state%cycle_count = 0
309 state%consecutive_failures = 0
310 state%active = .false.
311 state%running = .false.
312 state%stopped = .false.
313 end function clear_devloop_state
314
315 subroutine start_devloop(state, options)
316 type(devloop_state), intent(inout) :: state
317 type(devloop_options), intent(in), optional :: options
318
319 state = clear_devloop_state()
320 if (present(options)) then
321 state%options = options
322 end if
323 call normalize_options(state%options)
324 state%active = .true.
325 end subroutine start_devloop
326
327 subroutine stop_devloop(state)
328 type(devloop_state), intent(inout) :: state
329
330 state%active = .false.
331 state%running = .false.
332 state%stopped = .true.
333 end subroutine stop_devloop
334
335 function should_start_on_open(state) result(should_start)
336 type(devloop_state), intent(in) :: state
337 logical :: should_start
338
339 should_start = state%active .and. state%options%run_on_start .and. &
340 .not. state%running .and. .not. state%stopped
341 end function should_start_on_open
342
343 function devloop_start_trigger() result(trigger)
344 type(devloop_trigger) :: trigger
345
346 trigger = clear_devloop_trigger()
347 trigger%kind = FGOF_DEVLOOP_TRIGGER_START
348 trigger%reason = "start"
349 end function devloop_start_trigger
350
351 function devloop_change_trigger(change_count, reason) result(trigger)
352 integer, intent(in) :: change_count
353 character(len=*), intent(in), optional :: reason
354 type(devloop_trigger) :: trigger
355
356 trigger = clear_devloop_trigger()
357 if (change_count <= 0) return
358
359 trigger%kind = FGOF_DEVLOOP_TRIGGER_CHANGE
360 trigger%change_count = change_count
361 if (present(reason)) then
362 trigger%reason = reason
363 else
364 trigger%reason = "change"
365 end if
366 end function devloop_change_trigger
367
368 function devloop_manual_trigger(reason) result(trigger)
369 character(len=*), intent(in), optional :: reason
370 type(devloop_trigger) :: trigger
371
372 trigger = clear_devloop_trigger()
373 trigger%kind = FGOF_DEVLOOP_TRIGGER_MANUAL
374 if (present(reason)) then
375 trigger%reason = reason
376 else
377 trigger%reason = "manual"
378 end if
379 end function devloop_manual_trigger
380
381 function devloop_build_command(command_value, options, label) result(spec)
382 type(process_command), intent(in) :: command_value
383 type(process_options), intent(in), optional :: options
384 character(len=*), intent(in), optional :: label
385 type(devloop_command_spec) :: spec
386
387 spec = make_devloop_command(FGOF_DEVLOOP_COMMAND_BUILD, command_value, options, label)
388 end function devloop_build_command
389
390 function devloop_run_command(command_value, options, label) result(spec)
391 type(process_command), intent(in) :: command_value
392 type(process_options), intent(in), optional :: options
393 character(len=*), intent(in), optional :: label
394 type(devloop_command_spec) :: spec
395
396 spec = make_devloop_command(FGOF_DEVLOOP_COMMAND_RUN, command_value, options, label)
397 end function devloop_run_command
398
399 function devloop_smoke_command(command_value, options, label) result(spec)
400 type(process_command), intent(in) :: command_value
401 type(process_options), intent(in), optional :: options
402 character(len=*), intent(in), optional :: label
403 type(devloop_command_spec) :: spec
404
405 spec = make_devloop_command(FGOF_DEVLOOP_COMMAND_SMOKE, command_value, options, label)
406 end function devloop_smoke_command
407
408 function devloop_service_job(command, argv, label, background, new_process_group, signal_scope, &
409 terminal_handoff, resume_sends_sigcont, stop_before_restart, &
410 release_on_handoff) result(spec)
411 character(len=*), intent(in) :: command
412 character(len=*), intent(in), optional :: argv(:)
413 character(len=*), intent(in), optional :: label
414 logical, intent(in), optional :: background
415 logical, intent(in), optional :: new_process_group
416 integer, intent(in), optional :: signal_scope
417 integer, intent(in), optional :: terminal_handoff
418 logical, intent(in), optional :: resume_sends_sigcont
419 logical, intent(in), optional :: stop_before_restart
420 logical, intent(in), optional :: release_on_handoff
421 type(devloop_job_spec) :: spec
422 logical :: actual_background
423 logical :: actual_new_process_group
424 integer :: actual_signal_scope
425 integer :: actual_terminal_handoff
426 logical :: actual_resume_sends_sigcont
427
428 spec = clear_devloop_job_spec()
429
430 actual_background = .true.
431 if (present(background)) actual_background = background
432 actual_new_process_group = .true.
433 if (present(new_process_group)) actual_new_process_group = new_process_group
434 actual_signal_scope = FGOF_JOBS_SIGNAL_SCOPE_GROUP
435 if (present(signal_scope)) actual_signal_scope = signal_scope
436 actual_terminal_handoff = FGOF_JOBS_TERMINAL_HANDOFF_FOREGROUND
437 if (present(terminal_handoff)) actual_terminal_handoff = terminal_handoff
438 actual_resume_sends_sigcont = .true.
439 if (present(resume_sends_sigcont)) actual_resume_sends_sigcont = resume_sends_sigcont
440
441 if (present(argv)) then
442 spec%job = make_job_spec(command, argv, background=actual_background, &
443 new_process_group=actual_new_process_group, &
444 signal_scope=actual_signal_scope, &
445 terminal_handoff=actual_terminal_handoff, &
446 resume_sends_sigcont=actual_resume_sends_sigcont)
447 else
448 spec%job = make_job_spec(command, background=actual_background, &
449 new_process_group=actual_new_process_group, &
450 signal_scope=actual_signal_scope, &
451 terminal_handoff=actual_terminal_handoff, &
452 resume_sends_sigcont=actual_resume_sends_sigcont)
453 end if
454
455 spec%enabled = allocated(spec%job%command)
456 if (present(stop_before_restart)) spec%stop_before_restart = stop_before_restart
457 if (present(release_on_handoff)) spec%release_on_handoff = release_on_handoff
458 if (present(label)) then
459 spec%label = label
460 else if (allocated(spec%job%command)) then
461 spec%label = spec%job%command
462 end if
463 end function devloop_service_job
464
465 function devloop_summarize_watch_events(events) result(summary)
466 type(watch_event), intent(in) :: events(:)
467 type(devloop_watch_summary) :: summary
468 integer :: index_value
469
470 summary = clear_devloop_watch_summary()
471 summary%event_count = size(events)
472
473 do index_value = 1, size(events)
474 select case (events(index_value)%kind)
475 case (FGOF_WATCH_EVT_CREATED)
476 summary%created_count = summary%created_count + 1
477 case (FGOF_WATCH_EVT_MODIFIED)
478 summary%modified_count = summary%modified_count + 1
479 case (FGOF_WATCH_EVT_REMOVED)
480 summary%removed_count = summary%removed_count + 1
481 case (FGOF_WATCH_EVT_MOVED)
482 summary%moved_count = summary%moved_count + 1
483 case default
484 summary%ignored_none_count = summary%ignored_none_count + 1
485 cycle
486 end select
487
488 summary%change_count = summary%change_count + 1
489 if (events(index_value)%is_directory) then
490 summary%directory_change_count = summary%directory_change_count + 1
491 else
492 summary%file_change_count = summary%file_change_count + 1
493 end if
494 end do
495
496 summary%has_changes = summary%change_count > 0
497 end function devloop_summarize_watch_events
498
499 function devloop_watch_failure_summary(error_code, message) result(summary)
500 integer, intent(in) :: error_code
501 character(len=*), intent(in) :: message
502 type(devloop_watch_summary) :: summary
503
504 summary = clear_devloop_watch_summary()
505 summary%watch_error_code = error_code
506 summary%watch_failed = error_code /= 0
507 summary%watch_error_message = message
508 end function devloop_watch_failure_summary
509
510 function devloop_watch_options(options) result(watch_config)
511 type(devloop_options), intent(in), optional :: options
512 type(watch_options) :: watch_config
513 type(devloop_options) :: local_options
514
515 local_options = clear_devloop_options()
516 if (present(options)) local_options = options
517 call normalize_options(local_options)
518
519 watch_config = watch_options()
520 watch_config%debounce_polls = local_options%debounce_polls
521 watch_config%ignore_hidden = local_options%ignore_hidden
522 watch_config%emit_directory_events = local_options%restart_on_directory_change
523 end function devloop_watch_options
524
525 function devloop_watch_trigger(summary, options, reason) result(trigger)
526 type(devloop_watch_summary), intent(in) :: summary
527 type(devloop_options), intent(in), optional :: options
528 character(len=*), intent(in), optional :: reason
529 type(devloop_trigger) :: trigger
530 type(devloop_options) :: local_options
531 integer :: effective_change_count
532
533 trigger = clear_devloop_trigger()
534 if (summary%watch_failed) return
535 if (.not. summary%has_changes) return
536
537 local_options = clear_devloop_options()
538 if (present(options)) local_options = options
539 call normalize_options(local_options)
540
541 if (.not. local_options%restart_on_change) return
542
543 effective_change_count = summary%change_count
544 if (.not. local_options%restart_on_directory_change) then
545 effective_change_count = summary%file_change_count
546 end if
547
548 if (effective_change_count < local_options%min_restart_changes) return
549
550 if (present(reason)) then
551 trigger = devloop_change_trigger(effective_change_count, reason)
552 else
553 trigger = devloop_change_trigger(effective_change_count, "watch")
554 end if
555 end function devloop_watch_trigger
556
557 function run_devloop_command(spec) result(command_result)
558 type(devloop_command_spec), intent(in) :: spec
559 type(devloop_command_result) :: command_result
560 type(process_result) :: process_run_result
561
562 command_result = clear_devloop_command_result()
563 command_result%kind = spec%kind
564 command_result%label = command_kind_label(spec%kind)
565 if (allocated(spec%label)) then
566 if (len(spec%label) > 0) command_result%label = spec%label
567 end if
568
569 if (.not. spec%enabled) return
570
571 command_result%requested = .true.
572 command_result%skipped = .false.
573 process_run_result = run_process(spec%command, spec%options)
574 command_result%process = process_run_result
575 command_result%launched = process_run_result%launched
576 command_result%completed = process_run_result%completed
577 command_result%timed_out = process_run_result%timed_out
578 command_result%exit_code = process_run_result%exit_code
579 command_result%process_error_code = process_run_result%error_code
580 command_result%error_message = process_run_result%error_message
581 command_result%succeeded = process_run_result%error_code == FGOF_PROCESS_OK .and. &
582 process_run_result%completed .and. &
583 process_run_result%exited_normally .and. &
584 process_run_result%exit_code == 0
585 end function run_devloop_command
586
587 function run_devloop_cycle(state, trigger, build_command, run_command, smoke_command) result(supervision)
588 type(devloop_state), intent(inout) :: state
589 type(devloop_trigger), intent(in) :: trigger
590 type(devloop_command_spec), intent(in), optional :: build_command
591 type(devloop_command_spec), intent(in), optional :: run_command
592 type(devloop_command_spec), intent(in), optional :: smoke_command
593 type(devloop_supervision_result) :: supervision
594 logical :: should_continue
595
596 supervision = clear_devloop_supervision_result()
597 supervision%cycle = begin_devloop_cycle(state, trigger)
598 supervision%started = supervision%cycle%started
599 if (.not. supervision%started) return
600
601 should_continue = .true.
602 if (present(build_command)) then
603 supervision%build = run_devloop_command(build_command)
604 call record_supervised_command(supervision, supervision%build, should_continue)
605 end if
606
607 if (should_continue .and. present(run_command)) then
608 supervision%run = run_devloop_command(run_command)
609 call record_supervised_command(supervision, supervision%run, should_continue)
610 end if
611
612 if (should_continue .and. present(smoke_command)) then
613 supervision%smoke = run_devloop_command(smoke_command)
614 call record_supervised_command(supervision, supervision%smoke, should_continue)
615 end if
616
617 supervision%succeeded = should_continue
618 supervision%failed = .not. should_continue
619 supervision%decision = finish_devloop_cycle(state, supervision%succeeded, supervision%last_exit_code)
620 end function run_devloop_cycle
621
622 function attach_devloop_job(spec, pid, process_group, owns_process, owns_process_group) result(job_state)
623 type(devloop_job_spec), intent(in) :: spec
624 integer, intent(in) :: pid
625 integer, intent(in), optional :: process_group
626 logical, intent(in), optional :: owns_process
627 logical, intent(in), optional :: owns_process_group
628 type(devloop_job_state) :: job_state
629 integer :: actual_process_group
630 logical :: actual_owns_process
631 logical :: actual_owns_process_group
632
633 job_state = clear_devloop_job_state()
634 if (.not. spec%enabled) return
635
636 job_state%spec = spec
637 call configure_job(job_state%handle, spec%job)
638
639 actual_process_group = pid
640 if (present(process_group)) actual_process_group = process_group
641 actual_owns_process = .true.
642 if (present(owns_process)) actual_owns_process = owns_process
643 actual_owns_process_group = actual_owns_process .and. spec%job%new_process_group .and. &
644 actual_process_group == pid
645 if (present(owns_process_group)) actual_owns_process_group = owns_process_group
646
647 call attach_job(job_state%handle, pid, process_group=actual_process_group, &
648 owns_process=actual_owns_process, owns_process_group=actual_owns_process_group)
649 call refresh_devloop_job_state(job_state)
650 end function attach_devloop_job
651
652 subroutine attach_devloop_pipeline_members(job_state, pids)
653 type(devloop_job_state), intent(inout) :: job_state
654 integer, intent(in) :: pids(:)
655
656 call attach_pipeline_members(job_state%handle, pids)
657 call refresh_devloop_job_state(job_state)
658 end subroutine attach_devloop_pipeline_members
659
660 subroutine observe_devloop_job(job_state, result_value)
661 type(devloop_job_state), intent(inout) :: job_state
662 type(job_result), intent(in) :: result_value
663
664 call observe_wait_result(job_state%handle, result_value)
665 call refresh_devloop_job_state(job_state)
666 end subroutine observe_devloop_job
667
668 subroutine release_devloop_job(job_state)
669 type(devloop_job_state), intent(inout) :: job_state
670
671 call release_job(job_state%handle)
672 job_state%released = .true.
673 call refresh_devloop_job_state(job_state)
674 end subroutine release_devloop_job
675
676 function devloop_job_restart_plan(job_state, trigger) result(plan)
677 type(devloop_job_state), intent(in) :: job_state
678 type(devloop_trigger), intent(in) :: trigger
679 type(devloop_job_plan) :: plan
680
681 plan = clear_devloop_job_plan()
682 plan%pid = job_state%pid
683 plan%process_group = job_state%process_group
684 plan%signal_scope = job_state%signal_scope
685 plan%cleanup_needed = job_state%cleanup_needed
686 plan%terminal_handoff_required = job_state%terminal_handoff_required
687
688 if (.not. job_state%spec%enabled) return
689 if (.not. job_state%configured) return
690 if (trigger%kind == FGOF_DEVLOOP_TRIGGER_NONE) return
691 if (job_state%released) then
692 plan%reason = "job released"
693 return
694 end if
695
696 if (job_state%terminal_handoff_required .and. job_state%spec%release_on_handoff) then
697 plan%should_release = .true.
698 plan%reason = "release for terminal handoff"
699 return
700 end if
701
702 if (job_state%running .or. job_state%stopped .or. job_state%cleanup_needed) then
703 plan%action = FGOF_DEVLOOP_JOB_ACTION_RESTART
704 plan%should_restart = .true.
705 plan%should_start = .true.
706 plan%should_stop = job_state%spec%stop_before_restart
707 plan%reason = "restart existing job"
708 else
709 plan%action = FGOF_DEVLOOP_JOB_ACTION_START
710 plan%should_start = .true.
711 plan%reason = "start job"
712 end if
713 end function devloop_job_restart_plan
714
715 function begin_devloop_cycle(state, trigger) result(cycle)
716 type(devloop_state), intent(inout) :: state
717 type(devloop_trigger), intent(in) :: trigger
718 type(devloop_cycle) :: cycle
719
720 cycle = clear_devloop_cycle()
721 if (.not. state%active) return
722 if (state%running) return
723 if (state%stopped) return
724 if (trigger%kind == FGOF_DEVLOOP_TRIGGER_NONE) return
725 if (trigger%kind == FGOF_DEVLOOP_TRIGGER_CHANGE .and. .not. state%options%restart_on_change) return
726
727 state%cycle_count = state%cycle_count + 1
728 state%running = .true.
729
730 cycle%id = state%cycle_count
731 cycle%trigger_kind = trigger%kind
732 cycle%change_count = trigger%change_count
733 cycle%started = .true.
734 cycle%reason = trigger%reason
735 state%last_cycle = cycle
736 end function begin_devloop_cycle
737
738 function finish_devloop_cycle(state, succeeded, exit_code) result(decision)
739 type(devloop_state), intent(inout) :: state
740 logical, intent(in) :: succeeded
741 integer, intent(in), optional :: exit_code
742 type(devloop_decision) :: decision
743 integer :: actual_exit_code
744
745 decision = clear_devloop_decision()
746 if (.not. state%running) return
747
748 actual_exit_code = 0
749 if (present(exit_code)) actual_exit_code = exit_code
750
751 state%running = .false.
752 state%last_cycle%finished = .true.
753 state%last_cycle%succeeded = succeeded
754 state%last_cycle%exit_code = actual_exit_code
755
756 decision%cycle_id = state%last_cycle%id
757
758 if (succeeded) then
759 state%consecutive_failures = 0
760 decision%kind = FGOF_DEVLOOP_DECISION_IDLE
761 decision%reason = "cycle succeeded"
762 return
763 end if
764
765 state%consecutive_failures = state%consecutive_failures + 1
766 decision%failure_count = state%consecutive_failures
767 if (state%options%stop_on_failure .or. failure_limit_reached(state)) then
768 decision%kind = FGOF_DEVLOOP_DECISION_STOP
769 decision%should_stop = .true.
770 decision%reason = "failure policy stopped the loop"
771 call stop_devloop(state)
772 else
773 decision%kind = FGOF_DEVLOOP_DECISION_RESTART
774 decision%should_run = .true.
775 decision%reason = "cycle failed; restart allowed"
776 end if
777 end function finish_devloop_cycle
778
779 function devloop_backend_name() result(name)
780 character(len=:), allocatable :: name
781
782 name = FGOF_DEVLOOP_BACKEND_MODEL
783 end function devloop_backend_name
784
785 function make_devloop_command(kind, command_value, options, label) result(spec)
786 integer, intent(in) :: kind
787 type(process_command), intent(in) :: command_value
788 type(process_options), intent(in), optional :: options
789 character(len=*), intent(in), optional :: label
790 type(devloop_command_spec) :: spec
791
792 spec = clear_devloop_command_spec()
793 spec%kind = kind
794 spec%enabled = .true.
795 spec%command = command_value
796 spec%options = process_options()
797 if (present(options)) spec%options = options
798 if (present(label)) then
799 spec%label = label
800 else
801 spec%label = command_kind_label(kind)
802 end if
803 end function make_devloop_command
804
805 function clear_process_result() result(process_run_result)
806 type(process_result) :: process_run_result
807
808 process_run_result%launched = .false.
809 process_run_result%completed = .false.
810 process_run_result%timed_out = .false.
811 process_run_result%exited_normally = .false.
812 process_run_result%exit_code = -1
813 process_run_result%term_signal = 0
814 process_run_result%stdout = ""
815 process_run_result%stderr = ""
816 process_run_result%error_code = FGOF_PROCESS_OK
817 process_run_result%error_message = ""
818 process_run_result%elapsed_ms = 0
819 end function clear_process_result
820
821 function command_kind_label(kind) result(label)
822 integer, intent(in) :: kind
823 character(len=:), allocatable :: label
824
825 select case (kind)
826 case (FGOF_DEVLOOP_COMMAND_BUILD)
827 label = "build"
828 case (FGOF_DEVLOOP_COMMAND_RUN)
829 label = "run"
830 case (FGOF_DEVLOOP_COMMAND_SMOKE)
831 label = "smoke"
832 case default
833 label = "none"
834 end select
835 end function command_kind_label
836
837 subroutine record_supervised_command(supervision, command_result, should_continue)
838 type(devloop_supervision_result), intent(inout) :: supervision
839 type(devloop_command_result), intent(in) :: command_result
840 logical, intent(inout) :: should_continue
841
842 if (.not. command_result%requested) return
843
844 supervision%command_count = supervision%command_count + 1
845 supervision%last_exit_code = command_result%exit_code
846
847 if (command_result%succeeded) return
848
849 should_continue = .false.
850 supervision%failed_command_kind = command_result%kind
851 supervision%process_error_code = command_result%process_error_code
852 supervision%timed_out = command_result%timed_out
853 end subroutine record_supervised_command
854
855 subroutine refresh_devloop_job_state(job_state)
856 type(devloop_job_state), intent(inout) :: job_state
857 logical :: was_released
858
859 was_released = job_state%released
860 job_state%configured = job_is_configured(job_state%handle)
861 job_state%attached = job_state%handle%pid > 0
862 job_state%pid = job_state%handle%pid
863 job_state%process_group = job_state%handle%process_group
864 job_state%signal_scope = job_signal_scope(job_state%handle)
865 job_state%running = job_is_running(job_state%handle)
866 job_state%stopped = job_is_stopped(job_state%handle)
867 job_state%finished = job_is_finished(job_state%handle)
868 job_state%cleanup_needed = job_needs_cleanup(job_state%handle)
869 job_state%owns_process_group = job_owns_process_group(job_state%handle)
870 job_state%terminal_handoff_required = job_requires_terminal_handoff(job_state%handle)
871 job_state%released = was_released
872 job_state%label = ""
873 if (allocated(job_state%spec%label)) job_state%label = job_state%spec%label
874 end subroutine refresh_devloop_job_state
875
876 subroutine normalize_options(options)
877 type(devloop_options), intent(inout) :: options
878
879 if (options%max_failures < 0) options%max_failures = 0
880 if (options%min_restart_changes < 1) options%min_restart_changes = 1
881 if (options%debounce_polls < 0) options%debounce_polls = 0
882 end subroutine normalize_options
883
884 logical function failure_limit_reached(state) result(reached)
885 type(devloop_state), intent(in) :: state
886
887 reached = state%options%max_failures > 0 .and. &
888 state%consecutive_failures >= state%options%max_failures
889 end function failure_limit_reached
890
891 end module fgof_devloop
892