fix(apiserver): send the requested signal, force kill and checkpoint options (bug-1940)
Cause: ControlJob (backend/clib.go) looked its content argument up as a signal name, but the job control handlers pass a JSON JobControlEvent, so the lookup always missed and every kill sent TERM (stop TSTP, resume CONT); sig.force and ckpt.kill were hard-coded to 0 and the checkpoint method was never set. Fix: new pure jobCtrlParams (backend/convert_pure.go) decodes the event (or a bare signal name/number), validates the signal against JobSignalMapTable and returns an error instead of falling back, and maps force_kill to xce_signal_t.force (kill) or xce_ckpt_options_t.kill (checkpoint) plus the checkpoint method. ControlJob fills the C structs from it. Tests: backend/jobctrl_params_test.go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f164b8205
commit
4d2e80c4bb
3 changed files with 236 additions and 25 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -215,6 +216,107 @@ func jobCtrlUnionDiscriminant(action int) int {
|
|||
return 0 // JobControl_u_siginfo (stop/resume/kill all signal the job)
|
||||
}
|
||||
|
||||
// jobCtrlArgs is what ControlJob (clib.go) puts into xce_jobctrl_t's union
|
||||
// branch: Signo/Force fill xce_signal_t (stop/resume/kill), CkptKill/Method
|
||||
// fill xce_ckpt_options_t (checkpoint).
|
||||
type jobCtrlArgs struct {
|
||||
Signo int
|
||||
Force int
|
||||
CkptKill int
|
||||
Method string
|
||||
}
|
||||
|
||||
// jobCtrlParams maps a job action and ControlJob's content argument to the
|
||||
// values sent to the controller. content is either the JSON-encoded
|
||||
// models.JobControlEvent the job control handlers pass (controllers/jobs.go)
|
||||
// or, for older callers, a bare signal name or number; empty means the
|
||||
// action's default signal.
|
||||
//
|
||||
// bug-1940: content used to be looked up only as a signal name, so the JSON
|
||||
// event always missed and every kill sent TERM with force 0, and checkpoint
|
||||
// kill/method were never set. An unknown signal is now an error instead of a
|
||||
// silent fallback to the default.
|
||||
func jobCtrlParams(action int, content string) (jobCtrlArgs, error) {
|
||||
var (
|
||||
args jobCtrlArgs
|
||||
signo int
|
||||
forceKill bool
|
||||
)
|
||||
|
||||
content = strings.TrimSpace(content)
|
||||
if strings.HasPrefix(content, "{") {
|
||||
var evt models.JobControlEvent
|
||||
|
||||
if err := json.Unmarshal([]byte(content), &evt); err != nil {
|
||||
return args, fmt.Errorf("invalid job control content: %w", err)
|
||||
}
|
||||
if evt.Signal != 0 && !isKnownSignal(evt.Signal) {
|
||||
return args, fmt.Errorf("unsupported signal: %d", evt.Signal)
|
||||
}
|
||||
signo, forceKill, args.Method = evt.Signal, evt.ForceKill, evt.Method
|
||||
} else if content != "" {
|
||||
n, err := parseSignal(content)
|
||||
if err != nil {
|
||||
return args, err
|
||||
}
|
||||
signo = n
|
||||
}
|
||||
|
||||
switch action {
|
||||
case models.JOB_STOP, models.JOB_RESUME, models.JOB_KILL:
|
||||
if signo == 0 {
|
||||
signo = defaultActionSignal(action)
|
||||
}
|
||||
args.Signo = signo
|
||||
if action == models.JOB_KILL && forceKill {
|
||||
args.Force = 1
|
||||
}
|
||||
case models.JOB_CHKPNT:
|
||||
if forceKill {
|
||||
args.CkptKill = 1
|
||||
}
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// defaultActionSignal is the signal a stop/resume/kill sends when the caller
|
||||
// gave none.
|
||||
func defaultActionSignal(action int) int {
|
||||
switch action {
|
||||
case models.JOB_STOP:
|
||||
return models.JobSignalMapTable["TSTP"] // 20
|
||||
case models.JOB_RESUME:
|
||||
return models.JobSignalMapTable["CONT"] // 18
|
||||
}
|
||||
return models.JobSignalMapTable["TERM"] // 15
|
||||
}
|
||||
|
||||
// isKnownSignal reports whether n is one of models.JobSignalMapTable's values.
|
||||
func isKnownSignal(n int) bool {
|
||||
for _, v := range models.JobSignalMapTable {
|
||||
if v == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseSignal accepts a signal number or name (case-insensitive, with or
|
||||
// without the SIG prefix) and returns its number.
|
||||
func parseSignal(s string) (int, error) {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
if !isKnownSignal(n) {
|
||||
return 0, fmt.Errorf("unsupported signal: %s", s)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
name := strings.TrimPrefix(strings.ToUpper(s), "SIG")
|
||||
if n, ok := models.JobSignalMapTable[name]; ok {
|
||||
return n, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unsupported signal: %s", s)
|
||||
}
|
||||
|
||||
// resolveExecUser returns the effective exec user for a job submission: the
|
||||
// caller-specified ExecUser if present, otherwise the submitting user
|
||||
// (bug-1780: an unset execuser in the startjob request reaches xcecd as
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue