Fortran · 14237 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: job_control
3 ! Purpose: Job control management
4 ! ==============================================================================
5 module job_control
6 use shell_types
7 use system_interface
8 use iso_fortran_env, only: output_unit, error_unit
9 implicit none
10
11 contains
12
13 function add_job(shell, pgid, command_line, foreground) result(job_id)
14 type(shell_state_t), intent(inout) :: shell
15 integer(c_pid_t), intent(in) :: pgid
16 character(len=*), intent(in) :: command_line
17 logical, intent(in) :: foreground
18 integer :: job_id
19
20 integer :: i
21
22 ! Find empty slot or add new job
23 job_id = 0
24 do i = 1, MAX_JOBS
25 if (shell%jobs(i)%job_id == 0) then
26 job_id = shell%next_job_id
27 shell%next_job_id = shell%next_job_id + 1
28 shell%jobs(i)%job_id = job_id
29 shell%jobs(i)%pgid = pgid
30 shell%jobs(i)%command_line = command_line
31 shell%jobs(i)%state = JOB_RUNNING
32 shell%jobs(i)%foreground = foreground
33 shell%jobs(i)%notified = .false.
34 allocate(shell%jobs(i)%pids(1))
35 shell%jobs(i)%pids(1) = pgid
36 shell%jobs(i)%num_pids = 1
37
38 ! Update current/previous job tracking
39 if (shell%current_job_id /= 0) then
40 shell%previous_job_id = shell%current_job_id
41 end if
42 shell%current_job_id = job_id
43
44 exit
45 end if
46 end do
47
48 if (job_id > 0) shell%num_jobs = shell%num_jobs + 1
49 end function
50
51 subroutine remove_job(shell, job_id)
52 type(shell_state_t), intent(inout) :: shell
53 integer, intent(in) :: job_id
54 integer :: i
55
56 do i = 1, MAX_JOBS
57 if (shell%jobs(i)%job_id == job_id) then
58 if (allocated(shell%jobs(i)%pids)) deallocate(shell%jobs(i)%pids)
59 shell%jobs(i)%job_id = 0
60 shell%num_jobs = shell%num_jobs - 1
61 exit
62 end if
63 end do
64 end subroutine
65
66 subroutine update_job_status(shell)
67 use iso_fortran_env, only: error_unit
68 type(shell_state_t), intent(inout) :: shell
69 integer :: i, j
70 integer(c_int), target :: status
71 integer(c_pid_t) :: pid
72
73 do i = 1, MAX_JOBS
74 if (shell%jobs(i)%job_id > 0) then
75 do j = 1, shell%jobs(i)%num_pids
76 pid = c_waitpid(shell%jobs(i)%pids(j), c_loc(status), WNOHANG + WUNTRACED)
77
78 if (pid > 0) then
79 if (WIFEXITED(status)) then
80 shell%jobs(i)%state = JOB_DONE
81 ! DON'T set last_exit_status for background jobs!
82 else if (WIFSIGNALED(status)) then
83 shell%jobs(i)%state = JOB_DONE
84 ! DON'T set last_exit_status for background jobs!
85 else if (WIFSTOPPED(status)) then
86 shell%jobs(i)%state = JOB_STOPPED
87 end if
88 end if
89 end do
90 end if
91 end do
92 end subroutine
93
94 subroutine notify_job_status(shell)
95 type(shell_state_t), intent(inout) :: shell
96 integer :: i
97
98 do i = 1, MAX_JOBS
99 if (shell%jobs(i)%job_id > 0 .and. .not. shell%jobs(i)%notified) then
100 if (shell%jobs(i)%state == JOB_DONE) then
101 write(output_unit, '(a,i0,a,a)') '[', shell%jobs(i)%job_id, '] Done ', &
102 trim(shell%jobs(i)%command_line)
103 shell%jobs(i)%notified = .true.
104 call remove_job(shell, shell%jobs(i)%job_id)
105 else if (shell%jobs(i)%state == JOB_STOPPED) then
106 write(output_unit, '(a,i0,a,a)') '[', shell%jobs(i)%job_id, '] Stopped ', &
107 trim(shell%jobs(i)%command_line)
108 shell%jobs(i)%notified = .true.
109 end if
110 end if
111 end do
112 end subroutine
113
114 subroutine put_job_foreground(shell, job_id, cont)
115 type(shell_state_t), intent(inout) :: shell
116 integer, intent(in) :: job_id
117 logical, intent(in) :: cont
118
119 integer :: i, ret
120 integer(c_int), target :: status
121
122 do i = 1, MAX_JOBS
123 if (shell%jobs(i)%job_id == job_id) then
124 ! Give terminal to job
125 ret = c_tcsetpgrp(shell%shell_terminal, shell%jobs(i)%pgid)
126
127 ! Continue job if necessary
128 if (cont .and. shell%jobs(i)%state == JOB_STOPPED) then
129 ret = c_kill(-shell%jobs(i)%pgid, SIGCONT)
130 shell%jobs(i)%state = JOB_RUNNING
131 end if
132
133 ! Wait for job
134 shell%jobs(i)%foreground = .true.
135 ret = c_waitpid(-shell%jobs(i)%pgid, c_loc(status), WUNTRACED)
136
137 ! Take back terminal
138 ret = c_tcsetpgrp(shell%shell_terminal, shell%shell_pgid)
139
140 ! Update status
141 if (WIFEXITED(status)) then
142 shell%jobs(i)%state = JOB_DONE
143 shell%last_exit_status = WEXITSTATUS(status)
144 call remove_job(shell, job_id)
145 else if (WIFSIGNALED(status)) then
146 shell%jobs(i)%state = JOB_DONE
147 shell%last_exit_status = 128 + WTERMSIG(status)
148 call remove_job(shell, job_id)
149 else if (WIFSTOPPED(status)) then
150 shell%jobs(i)%state = JOB_STOPPED
151 write(output_unit, '(a)') 'Stopped'
152 end if
153
154 exit
155 end if
156 end do
157 end subroutine
158
159 subroutine put_job_background(shell, job_id, cont)
160 type(shell_state_t), intent(inout) :: shell
161 integer, intent(in) :: job_id
162 logical, intent(in) :: cont
163
164 integer :: i, ret
165
166 do i = 1, MAX_JOBS
167 if (shell%jobs(i)%job_id == job_id) then
168 shell%jobs(i)%foreground = .false.
169
170 if (cont .and. shell%jobs(i)%state == JOB_STOPPED) then
171 ret = c_kill(-shell%jobs(i)%pgid, SIGCONT)
172 shell%jobs(i)%state = JOB_RUNNING
173 end if
174
175 exit
176 end if
177 end do
178 end subroutine
179
180 ! Enhanced job control functions
181 function find_job_by_id(shell, job_id) result(job_index)
182 type(shell_state_t), intent(in) :: shell
183 integer, intent(in) :: job_id
184 integer :: job_index
185 integer :: i
186
187 job_index = 0
188 do i = 1, MAX_JOBS
189 if (shell%jobs(i)%job_id == job_id) then
190 job_index = i
191 return
192 end if
193 end do
194 end function
195
196 function find_job_by_pgid(shell, pgid) result(job_index)
197 type(shell_state_t), intent(in) :: shell
198 integer(c_pid_t), intent(in) :: pgid
199 integer :: job_index
200 integer :: i
201
202 job_index = 0
203 do i = 1, MAX_JOBS
204 if (shell%jobs(i)%pgid == pgid .and. shell%jobs(i)%job_id > 0) then
205 job_index = i
206 return
207 end if
208 end do
209 end function
210
211 function find_job_pgid(shell, job_id) result(pgid)
212 type(shell_state_t), intent(in) :: shell
213 integer, intent(in) :: job_id
214 integer(c_pid_t) :: pgid
215 integer :: job_index
216
217 job_index = find_job_by_id(shell, job_id)
218 if (job_index > 0) then
219 pgid = shell%jobs(job_index)%pgid
220 else
221 pgid = 0
222 end if
223 end function
224
225 subroutine suspend_job(shell, job_id)
226 type(shell_state_t), intent(inout) :: shell
227 integer, intent(in) :: job_id
228 integer :: job_index
229 integer :: ret
230
231 job_index = find_job_by_id(shell, job_id)
232 if (job_index == 0) then
233 write(error_unit, '(a,i15,a)') 'Job ', job_id, ' not found'
234 shell%last_exit_status = 1
235 return
236 end if
237
238 if (shell%jobs(job_index)%state == JOB_STOPPED) then
239 write(error_unit, '(a,i15,a)') 'Job ', job_id, ' already stopped'
240 return
241 end if
242
243 ! Send SIGTSTP to the process group
244 ret = c_kill(-shell%jobs(job_index)%pgid, SIGTSTP)
245 if (ret == 0) then
246 shell%jobs(job_index)%state = JOB_STOPPED
247
248 ! Update current/previous job tracking
249 if (shell%current_job_id /= job_id) then
250 shell%previous_job_id = shell%current_job_id
251 end if
252 shell%current_job_id = job_id
253
254 write(output_unit, '(a,i15,a)') '[', job_id, '] Suspended'
255 else
256 write(error_unit, '(a,i15)') 'Failed to suspend job ', job_id
257 shell%last_exit_status = 1
258 end if
259 end subroutine
260
261 subroutine resume_job_bg(shell, job_id)
262 type(shell_state_t), intent(inout) :: shell
263 integer, intent(in) :: job_id
264 integer :: job_index
265 integer :: ret
266
267 job_index = find_job_by_id(shell, job_id)
268 if (job_index == 0) then
269 write(error_unit, '(a,i15,a)') 'Job ', job_id, ' not found'
270 shell%last_exit_status = 1
271 return
272 end if
273
274 if (shell%jobs(job_index)%state /= JOB_STOPPED) then
275 shell%last_exit_status = 1
276 return
277 end if
278
279 ! Send SIGCONT to the process group
280 ret = c_kill(-shell%jobs(job_index)%pgid, SIGCONT)
281 if (ret == 0) then
282 shell%jobs(job_index)%state = JOB_RUNNING
283 shell%jobs(job_index)%foreground = .false.
284 shell%jobs(job_index)%notified = .false.
285
286 ! Update current/previous job tracking
287 if (shell%current_job_id /= job_id) then
288 shell%previous_job_id = shell%current_job_id
289 end if
290 shell%current_job_id = job_id
291
292 write(output_unit, '(a,i15,a,a)') '[', job_id, '] ', trim(shell%jobs(job_index)%command_line), ' &'
293 else
294 write(error_unit, '(a,i15)') 'Failed to resume job in background ', job_id
295 shell%last_exit_status = 1
296 end if
297 end subroutine
298
299 subroutine resume_job_fg(shell, job_id)
300 type(shell_state_t), intent(inout) :: shell
301 integer, intent(in) :: job_id
302 integer :: job_index
303 integer :: ret
304 integer(c_int), target :: status
305
306 job_index = find_job_by_id(shell, job_id)
307 if (job_index == 0) then
308 write(error_unit, '(a,i15,a)') 'Job ', job_id, ' not found'
309 shell%last_exit_status = 1
310 return
311 end if
312
313 if (shell%jobs(job_index)%state /= JOB_STOPPED) then
314 shell%last_exit_status = 1
315 return
316 end if
317
318 ! Update current/previous job tracking before resuming
319 if (shell%current_job_id /= job_id) then
320 shell%previous_job_id = shell%current_job_id
321 end if
322 shell%current_job_id = job_id
323
324 ! Give terminal control to job
325 if (shell%is_interactive) then
326 ret = c_tcsetpgrp(shell%shell_terminal, shell%jobs(job_index)%pgid)
327 end if
328
329 ! Send SIGCONT to the process group
330 ret = c_kill(-shell%jobs(job_index)%pgid, SIGCONT)
331 if (ret == 0) then
332 shell%jobs(job_index)%state = JOB_RUNNING
333 shell%jobs(job_index)%foreground = .true.
334 shell%jobs(job_index)%notified = .false.
335 write(output_unit, '(a)') trim(shell%jobs(job_index)%command_line)
336
337 ! Wait for job to complete or stop
338 ret = c_waitpid(-shell%jobs(job_index)%pgid, c_loc(status), WUNTRACED)
339
340 ! Take back terminal control
341 if (shell%is_interactive) then
342 ret = c_tcsetpgrp(shell%shell_terminal, shell%shell_pgid)
343 end if
344
345 if (WIFEXITED(status)) then
346 shell%jobs(job_index)%state = JOB_DONE
347 shell%last_exit_status = WEXITSTATUS(status)
348 call remove_job(shell, job_id)
349 else if (WIFSIGNALED(status)) then
350 shell%jobs(job_index)%state = JOB_DONE
351 shell%last_exit_status = 128 + WTERMSIG(status)
352 call remove_job(shell, job_id)
353 else if (WIFSTOPPED(status)) then
354 shell%jobs(job_index)%state = JOB_STOPPED
355 shell%jobs(job_index)%foreground = .false.
356 write(output_unit, '(a)') 'Stopped'
357 end if
358 else
359 write(error_unit, '(a,i15)') 'Failed to resume job in foreground ', job_id
360 shell%last_exit_status = 1
361 end if
362 end subroutine
363
364 subroutine kill_job(shell, job_id, signal_num)
365 type(shell_state_t), intent(inout) :: shell
366 integer, intent(in) :: job_id
367 integer, intent(in), optional :: signal_num
368 integer :: job_index
369 integer :: ret, sig
370
371 job_index = find_job_by_id(shell, job_id)
372 if (job_index == 0) then
373 write(error_unit, '(a,i15,a)') 'Job ', job_id, ' not found'
374 shell%last_exit_status = 1
375 return
376 end if
377
378 sig = 15 ! Default signal: SIGTERM
379 if (present(signal_num)) sig = signal_num
380
381 ! Send signal to the process group
382 ret = c_kill(-shell%jobs(job_index)%pgid, sig)
383 if (ret == 0) then
384 if (sig == 9 .or. sig == 15) then ! SIGKILL or SIGTERM
385 shell%jobs(job_index)%state = JOB_DONE
386 call remove_job(shell, job_id)
387 end if
388 write(output_unit, '(a,i15,a)') '[', job_id, '] Terminated'
389 else
390 write(error_unit, '(a,i15)') 'Failed to kill job ', job_id
391 shell%last_exit_status = 1
392 end if
393 end subroutine
394
395 subroutine list_jobs(shell, show_pids)
396 type(shell_state_t), intent(in) :: shell
397 logical, intent(in), optional :: show_pids
398 logical :: show_pid_info
399 integer :: i
400 character(len=16) :: state_str
401
402 show_pid_info = .false.
403 if (present(show_pids)) show_pid_info = show_pids
404
405 do i = 1, MAX_JOBS
406 if (shell%jobs(i)%job_id > 0) then
407 select case(shell%jobs(i)%state)
408 case(JOB_RUNNING)
409 if (shell%jobs(i)%foreground) then
410 state_str = 'Running'
411 else
412 state_str = 'Running'
413 end if
414 case(JOB_STOPPED)
415 state_str = 'Stopped'
416 case(JOB_DONE)
417 state_str = 'Done'
418 end select
419
420 block
421 character(len=1) :: cur_mark
422 character(len=256) :: cmd_display
423 ! + for current job, - for previous, space otherwise
424 if (shell%jobs(i)%job_id == &
425 shell%current_job_id) then
426 cur_mark = '+'
427 else if (shell%jobs(i)%job_id == &
428 shell%previous_job_id) then
429 cur_mark = '-'
430 else
431 cur_mark = ' '
432 end if
433 ! Add trailing & for background running jobs
434 cmd_display = trim(shell%jobs(i)%command_line)
435 if (shell%jobs(i)%state == JOB_RUNNING .and. &
436 .not. shell%jobs(i)%foreground) then
437 cmd_display = trim(cmd_display) // ' &'
438 end if
439 if (show_pid_info) then
440 write(output_unit, '(i0)') shell%jobs(i)%pgid
441 else
442 write(output_unit, '(a,i0,a,a,2x,a,a,a)') &
443 '[', shell%jobs(i)%job_id, ']', cur_mark, &
444 trim(state_str), &
445 repeat(' ', max(1, &
446 27 - len_trim(state_str))), &
447 trim(cmd_display)
448 end if
449 end block
450 end if
451 end do
452 end subroutine
453
454 end module job_control